Use shared sacn library instead of local package
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
18
main.go
18
main.go
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/gopatchy/artnet"
|
||||
"github.com/gopatchy/artmap/config"
|
||||
"github.com/gopatchy/artmap/remap"
|
||||
"github.com/gopatchy/artmap/sacn"
|
||||
"github.com/gopatchy/sacn"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -162,10 +162,24 @@ func main() {
|
||||
// Create sACN receiver if needed
|
||||
sacnUniverses := cfg.SACNSourceUniverses()
|
||||
if len(sacnUniverses) > 0 {
|
||||
sacnReceiver, err := sacn.NewReceiver(sacnUniverses, *sacnInterface, app.HandleSACN)
|
||||
sacnReceiver, err := sacn.NewReceiver(*sacnInterface)
|
||||
if err != nil {
|
||||
log.Fatalf("sacn receiver error: %v", err)
|
||||
}
|
||||
var iface *net.Interface
|
||||
if *sacnInterface != "" {
|
||||
iface, _ = net.InterfaceByName(*sacnInterface)
|
||||
}
|
||||
for _, u := range sacnUniverses {
|
||||
if err := sacnReceiver.JoinUniverse(iface, u); err != nil {
|
||||
log.Printf("[sacn] failed to join universe %d: %v", u, err)
|
||||
}
|
||||
}
|
||||
sacnReceiver.SetHandler(func(src *net.UDPAddr, pkt interface{}) {
|
||||
if data, ok := pkt.(*sacn.DataPacket); ok {
|
||||
app.HandleSACN(data.Universe, data.Data)
|
||||
}
|
||||
})
|
||||
app.sacnReceiver = sacnReceiver
|
||||
sacnReceiver.Start()
|
||||
log.Printf("[sacn] listening universes=%v", sacnUniverses)
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package sacn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func FuzzParsePacket(f *testing.F) {
|
||||
cid := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
validPacket := BuildDataPacket(1, 0, "test", cid, make([]byte, 512))
|
||||
f.Add(validPacket)
|
||||
f.Add(BuildDataPacket(1, 0, "test", cid, make([]byte, 100)))
|
||||
f.Add(BuildDataPacket(63999, 255, "long source name here", cid, make([]byte, 512)))
|
||||
f.Add([]byte{})
|
||||
f.Add(make([]byte, 125))
|
||||
f.Add(make([]byte, 126))
|
||||
f.Add(make([]byte, 638))
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
_, dmxData, ok := ParsePacket(data)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if len(dmxData) != 512 {
|
||||
t.Fatalf("dmx data should be 512 bytes, got %d", len(dmxData))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzBuildParseRoundtrip(f *testing.F) {
|
||||
f.Add(uint16(1), uint8(0), "test", make([]byte, 512))
|
||||
f.Add(uint16(63999), uint8(255), "source", make([]byte, 100))
|
||||
f.Add(uint16(100), uint8(128), "", make([]byte, 0))
|
||||
f.Add(uint16(1), uint8(0), "a]very long source name that exceeds normal limits", make([]byte, 512))
|
||||
|
||||
f.Fuzz(func(t *testing.T, universe uint16, seq uint8, sourceName string, dmxInput []byte) {
|
||||
if universe < 1 || universe > 63999 {
|
||||
return
|
||||
}
|
||||
cid := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
packet := BuildDataPacket(universe, seq, sourceName, cid, dmxInput)
|
||||
parsedUniverse, parsedData, ok := ParsePacket(packet)
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse packet we just built")
|
||||
}
|
||||
if parsedUniverse != universe {
|
||||
t.Fatalf("universe mismatch: sent %d, got %d", universe, parsedUniverse)
|
||||
}
|
||||
expectedLen := len(dmxInput)
|
||||
if expectedLen > 512 {
|
||||
expectedLen = 512
|
||||
}
|
||||
if !bytes.Equal(parsedData[:expectedLen], dmxInput[:expectedLen]) {
|
||||
t.Fatalf("dmx data mismatch")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ParsePacket(data []byte) (universe uint16, dmxData [512]byte, ok bool) {
|
||||
if len(data) < 126 {
|
||||
return 0, dmxData, false
|
||||
}
|
||||
if data[4] != 0x41 || data[5] != 0x53 || data[6] != 0x43 {
|
||||
return 0, dmxData, false
|
||||
}
|
||||
rootVector := uint32(data[18])<<24 | uint32(data[19])<<16 | uint32(data[20])<<8 | uint32(data[21])
|
||||
if rootVector != VectorRootE131Data {
|
||||
return 0, dmxData, false
|
||||
}
|
||||
framingVector := uint32(data[40])<<24 | uint32(data[41])<<16 | uint32(data[42])<<8 | uint32(data[43])
|
||||
if framingVector != VectorE131DataPacket {
|
||||
return 0, dmxData, false
|
||||
}
|
||||
universe = uint16(data[113])<<8 | uint16(data[114])
|
||||
if data[117] != VectorDMPSetProperty {
|
||||
return 0, dmxData, false
|
||||
}
|
||||
propCount := uint16(data[123])<<8 | uint16(data[124])
|
||||
if propCount < 1 {
|
||||
return 0, dmxData, false
|
||||
}
|
||||
dmxLen := int(propCount) - 1
|
||||
if dmxLen > 512 {
|
||||
dmxLen = 512
|
||||
}
|
||||
if len(data) < 126+dmxLen {
|
||||
return 0, dmxData, false
|
||||
}
|
||||
copy(dmxData[:], data[126:126+dmxLen])
|
||||
return universe, dmxData, true
|
||||
}
|
||||
141
sacn/protocol.go
141
sacn/protocol.go
@@ -1,141 +0,0 @@
|
||||
package sacn
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
Port = 5568
|
||||
|
||||
ACNPacketIdentifier = 0x41534300
|
||||
|
||||
VectorRootE131Data = 0x00000004
|
||||
VectorRootE131Extended = 0x00000008
|
||||
VectorE131DataPacket = 0x00000002
|
||||
VectorE131Discovery = 0x00000002
|
||||
VectorDMPSetProperty = 0x02
|
||||
VectorUniverseDiscovery = 0x00000001
|
||||
)
|
||||
|
||||
var (
|
||||
// ACN packet identifier (12 bytes)
|
||||
packetIdentifier = [12]byte{
|
||||
0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00,
|
||||
}
|
||||
)
|
||||
|
||||
// BuildDataPacket creates an E1.31 (sACN) data packet
|
||||
func BuildDataPacket(universe uint16, sequence uint8, sourceName string, cid [16]byte, data []byte) []byte {
|
||||
dataLen := len(data)
|
||||
if dataLen > 512 {
|
||||
dataLen = 512
|
||||
}
|
||||
|
||||
// Total packet size: Root Layer (38) + Framing Layer (77) + DMP Layer (11 + data)
|
||||
// = 126 + dataLen
|
||||
pktLen := 126 + dataLen
|
||||
buf := make([]byte, pktLen)
|
||||
|
||||
// Root Layer (38 bytes)
|
||||
// Preamble Size (2 bytes)
|
||||
binary.BigEndian.PutUint16(buf[0:2], 0x0010)
|
||||
// Post-amble Size (2 bytes)
|
||||
binary.BigEndian.PutUint16(buf[2:4], 0x0000)
|
||||
// ACN Packet Identifier (12 bytes)
|
||||
copy(buf[4:16], packetIdentifier[:])
|
||||
// Flags and Length (2 bytes) - high 4 bits are flags (0x7), low 12 bits are length
|
||||
rootLen := pktLen - 16 // Length from after ACN Packet Identifier
|
||||
binary.BigEndian.PutUint16(buf[16:18], 0x7000|uint16(rootLen))
|
||||
// Vector (4 bytes)
|
||||
binary.BigEndian.PutUint32(buf[18:22], VectorRootE131Data)
|
||||
// CID (16 bytes)
|
||||
copy(buf[22:38], cid[:])
|
||||
|
||||
// Framing Layer (77 bytes, starting at offset 38)
|
||||
// Flags and Length (2 bytes)
|
||||
framingLen := pktLen - 38
|
||||
binary.BigEndian.PutUint16(buf[38:40], 0x7000|uint16(framingLen))
|
||||
// Vector (4 bytes)
|
||||
binary.BigEndian.PutUint32(buf[40:44], VectorE131DataPacket)
|
||||
// Source Name (64 bytes, null-terminated)
|
||||
copy(buf[44:108], sourceName)
|
||||
// Priority (1 byte)
|
||||
buf[108] = 100
|
||||
// Synchronization Address (2 bytes)
|
||||
binary.BigEndian.PutUint16(buf[109:111], 0)
|
||||
// Sequence Number (1 byte)
|
||||
buf[111] = sequence
|
||||
// Options (1 byte)
|
||||
buf[112] = 0
|
||||
// Universe (2 bytes)
|
||||
binary.BigEndian.PutUint16(buf[113:115], universe)
|
||||
|
||||
// DMP Layer (11 + dataLen bytes, starting at offset 115)
|
||||
// Flags and Length (2 bytes)
|
||||
dmpLen := 11 + dataLen
|
||||
binary.BigEndian.PutUint16(buf[115:117], 0x7000|uint16(dmpLen))
|
||||
// Vector (1 byte)
|
||||
buf[117] = VectorDMPSetProperty
|
||||
// Address Type & Data Type (1 byte)
|
||||
buf[118] = 0xa1
|
||||
// First Property Address (2 bytes)
|
||||
binary.BigEndian.PutUint16(buf[119:121], 0)
|
||||
// Address Increment (2 bytes)
|
||||
binary.BigEndian.PutUint16(buf[121:123], 1)
|
||||
// Property Value Count (2 bytes) - includes START code
|
||||
binary.BigEndian.PutUint16(buf[123:125], uint16(dataLen+1))
|
||||
// START Code (1 byte)
|
||||
buf[125] = 0
|
||||
// Property Values (DMX data)
|
||||
copy(buf[126:], data[:dataLen])
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func MulticastAddr(universe uint16) *net.UDPAddr {
|
||||
return &net.UDPAddr{
|
||||
IP: net.IPv4(239, 255, byte(universe>>8), byte(universe&0xff)),
|
||||
Port: Port,
|
||||
}
|
||||
}
|
||||
|
||||
var DiscoveryAddr = &net.UDPAddr{
|
||||
IP: net.IPv4(239, 255, 250, 214),
|
||||
Port: Port,
|
||||
}
|
||||
|
||||
func BuildDiscoveryPacket(sourceName string, cid [16]byte, page, lastPage uint8, universes []uint16) []byte {
|
||||
universeCount := len(universes)
|
||||
if universeCount > 512 {
|
||||
universeCount = 512
|
||||
}
|
||||
|
||||
pktLen := 120 + universeCount*2
|
||||
buf := make([]byte, pktLen)
|
||||
|
||||
binary.BigEndian.PutUint16(buf[0:2], 0x0010)
|
||||
binary.BigEndian.PutUint16(buf[2:4], 0x0000)
|
||||
copy(buf[4:16], packetIdentifier[:])
|
||||
rootLen := pktLen - 16
|
||||
binary.BigEndian.PutUint16(buf[16:18], 0x7000|uint16(rootLen))
|
||||
binary.BigEndian.PutUint32(buf[18:22], VectorRootE131Extended)
|
||||
copy(buf[22:38], cid[:])
|
||||
|
||||
framingLen := pktLen - 38
|
||||
binary.BigEndian.PutUint16(buf[38:40], 0x7000|uint16(framingLen))
|
||||
binary.BigEndian.PutUint32(buf[40:44], VectorE131Discovery)
|
||||
copy(buf[44:108], sourceName)
|
||||
binary.BigEndian.PutUint32(buf[108:112], 0)
|
||||
|
||||
discoveryLen := pktLen - 112
|
||||
binary.BigEndian.PutUint16(buf[112:114], 0x7000|uint16(discoveryLen))
|
||||
binary.BigEndian.PutUint32(buf[114:118], VectorUniverseDiscovery)
|
||||
buf[118] = page
|
||||
buf[119] = lastPage
|
||||
for i := 0; i < universeCount; i++ {
|
||||
binary.BigEndian.PutUint16(buf[120+i*2:122+i*2], universes[i])
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
143
sacn/receiver.go
143
sacn/receiver.go
@@ -1,143 +0,0 @@
|
||||
package sacn
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
// DMXHandler is called when DMX data is received
|
||||
type DMXHandler func(universe uint16, data [512]byte)
|
||||
|
||||
// Receiver listens for sACN packets
|
||||
type Receiver struct {
|
||||
conn *ipv4.PacketConn
|
||||
universes []uint16
|
||||
handler DMXHandler
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewReceiver creates a new sACN receiver for the given universes
|
||||
func NewReceiver(universes []uint16, ifaceName string, handler DMXHandler) (*Receiver, error) {
|
||||
c, err := net.ListenPacket("udp4", ":5568")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var iface *net.Interface
|
||||
if ifaceName != "" {
|
||||
iface, err = net.InterfaceByName(ifaceName)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
p := ipv4.NewPacketConn(c)
|
||||
|
||||
for _, u := range universes {
|
||||
group := net.IPv4(239, 255, byte(u>>8), byte(u&0xff))
|
||||
if err := p.JoinGroup(iface, &net.UDPAddr{IP: group}); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Receiver{
|
||||
conn: p,
|
||||
universes: universes,
|
||||
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, 638) // Max sACN packet size
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, _, _, err := r.conn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-r.done:
|
||||
return
|
||||
default:
|
||||
log.Printf("sacn read error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
r.handlePacket(buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Receiver) handlePacket(data []byte) {
|
||||
// Minimum packet size check
|
||||
if len(data) < 126 {
|
||||
return
|
||||
}
|
||||
|
||||
// Check ACN packet identifier
|
||||
if data[4] != 0x41 || data[5] != 0x53 || data[6] != 0x43 {
|
||||
return
|
||||
}
|
||||
|
||||
// Check root vector (E1.31 data)
|
||||
rootVector := binary.BigEndian.Uint32(data[18:22])
|
||||
if rootVector != VectorRootE131Data {
|
||||
return
|
||||
}
|
||||
|
||||
// Check framing vector (DMP data)
|
||||
framingVector := binary.BigEndian.Uint32(data[40:44])
|
||||
if framingVector != VectorE131DataPacket {
|
||||
return
|
||||
}
|
||||
|
||||
// Get universe
|
||||
universe := binary.BigEndian.Uint16(data[113:115])
|
||||
|
||||
// Check DMP vector
|
||||
if data[117] != VectorDMPSetProperty {
|
||||
return
|
||||
}
|
||||
|
||||
// Get property count (includes START code)
|
||||
propCount := binary.BigEndian.Uint16(data[123:125])
|
||||
if propCount < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip START code at data[125]
|
||||
dmxLen := int(propCount) - 1
|
||||
if dmxLen > 512 {
|
||||
dmxLen = 512
|
||||
}
|
||||
|
||||
if len(data) < 126+dmxLen {
|
||||
return
|
||||
}
|
||||
|
||||
var dmxData [512]byte
|
||||
copy(dmxData[:], data[126:126+dmxLen])
|
||||
|
||||
r.handler(universe, dmxData)
|
||||
}
|
||||
144
sacn/sender.go
144
sacn/sender.go
@@ -1,144 +0,0 @@
|
||||
package sacn
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
type Sender struct {
|
||||
conn *net.UDPConn
|
||||
sourceName string
|
||||
cid [16]byte
|
||||
sequences map[uint16]uint8
|
||||
seqMu sync.Mutex
|
||||
universes map[uint16]bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewSender creates a new sACN sender
|
||||
func NewSender(sourceName string, ifaceName string) (*Sender, error) {
|
||||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ifaceName != "" {
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
p := ipv4.NewPacketConn(conn)
|
||||
if err := p.SetMulticastInterface(iface); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var cid [16]byte
|
||||
rand.Read(cid[:])
|
||||
|
||||
return &Sender{
|
||||
conn: conn,
|
||||
sourceName: sourceName,
|
||||
cid: cid,
|
||||
sequences: make(map[uint16]uint8),
|
||||
universes: make(map[uint16]bool),
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendDMX sends DMX data to a universe via multicast
|
||||
func (s *Sender) SendDMX(universe uint16, data []byte) error {
|
||||
s.seqMu.Lock()
|
||||
seq := s.sequences[universe]
|
||||
s.sequences[universe] = seq + 1
|
||||
s.seqMu.Unlock()
|
||||
|
||||
pkt := BuildDataPacket(universe, seq, s.sourceName, s.cid, data)
|
||||
addr := MulticastAddr(universe)
|
||||
|
||||
_, err := s.conn.WriteToUDP(pkt, addr)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendDMXUnicast sends DMX data to a specific address
|
||||
func (s *Sender) SendDMXUnicast(addr *net.UDPAddr, universe uint16, data []byte) error {
|
||||
s.seqMu.Lock()
|
||||
seq := s.sequences[universe]
|
||||
s.sequences[universe] = seq + 1
|
||||
s.seqMu.Unlock()
|
||||
|
||||
pkt := BuildDataPacket(universe, seq, s.sourceName, s.cid, data)
|
||||
|
||||
_, err := s.conn.WriteToUDP(pkt, addr)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Sender) Close() error {
|
||||
select {
|
||||
case <-s.done:
|
||||
default:
|
||||
close(s.done)
|
||||
}
|
||||
return s.conn.Close()
|
||||
}
|
||||
|
||||
func (s *Sender) RegisterUniverse(universe uint16) {
|
||||
s.seqMu.Lock()
|
||||
s.universes[universe] = true
|
||||
s.seqMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Sender) StartDiscovery() {
|
||||
go s.discoveryLoop()
|
||||
}
|
||||
|
||||
func (s *Sender) discoveryLoop() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
s.sendDiscovery()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.sendDiscovery()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sender) sendDiscovery() {
|
||||
s.seqMu.Lock()
|
||||
universes := make([]uint16, 0, len(s.universes))
|
||||
for u := range s.universes {
|
||||
universes = append(universes, u)
|
||||
}
|
||||
s.seqMu.Unlock()
|
||||
|
||||
if len(universes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Slice(universes, func(i, j int) bool { return universes[i] < universes[j] })
|
||||
|
||||
const maxPerPage = 512
|
||||
totalPages := (len(universes) + maxPerPage - 1) / maxPerPage
|
||||
|
||||
for page := 0; page < totalPages; page++ {
|
||||
start := page * maxPerPage
|
||||
end := start + maxPerPage
|
||||
if end > len(universes) {
|
||||
end = len(universes)
|
||||
}
|
||||
pkt := BuildDiscoveryPacket(s.sourceName, s.cid, uint8(page), uint8(totalPages-1), universes[start:end])
|
||||
s.conn.WriteToUDP(pkt, DiscoveryAddr)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user