Use shared artnet library
This commit is contained in:
@@ -1,312 +0,0 @@
|
|||||||
package artnet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Node represents a discovered ArtNet node
|
|
||||||
type Node struct {
|
|
||||||
IP net.IP
|
|
||||||
Port uint16
|
|
||||||
ShortName string
|
|
||||||
LongName string
|
|
||||||
Universes []Universe
|
|
||||||
LastSeen time.Time
|
|
||||||
CanTransmit bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discovery handles ArtNet node discovery
|
|
||||||
type Discovery struct {
|
|
||||||
sender *Sender
|
|
||||||
receiver *Receiver
|
|
||||||
nodes map[string]*Node // keyed by IP string
|
|
||||||
nodesMu sync.RWMutex
|
|
||||||
localIP [4]byte
|
|
||||||
localMAC [6]byte
|
|
||||||
broadcast net.IP
|
|
||||||
shortName string
|
|
||||||
longName string
|
|
||||||
inputUnivs []Universe // universes we transmit TO (SwIn)
|
|
||||||
outputUnivs []Universe // universes we receive FROM (SwOut)
|
|
||||||
pollTargets []*net.UDPAddr
|
|
||||||
done chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDiscovery creates a new discovery handler
|
|
||||||
func NewDiscovery(sender *Sender, shortName, longName string, inputUnivs, outputUnivs []Universe, pollTargets []*net.UDPAddr) *Discovery {
|
|
||||||
return &Discovery{
|
|
||||||
sender: sender,
|
|
||||||
nodes: make(map[string]*Node),
|
|
||||||
shortName: shortName,
|
|
||||||
longName: longName,
|
|
||||||
inputUnivs: inputUnivs,
|
|
||||||
outputUnivs: outputUnivs,
|
|
||||||
pollTargets: pollTargets,
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start begins periodic discovery
|
|
||||||
func (d *Discovery) Start() {
|
|
||||||
d.detectInterface()
|
|
||||||
go d.pollLoop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops discovery
|
|
||||||
func (d *Discovery) Stop() {
|
|
||||||
close(d.done)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discovery) pollLoop() {
|
|
||||||
// Send initial poll to all targets
|
|
||||||
d.sendPolls()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
cleanupTicker := time.NewTicker(30 * time.Second)
|
|
||||||
defer cleanupTicker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-d.done:
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
d.sendPolls()
|
|
||||||
case <-cleanupTicker.C:
|
|
||||||
d.cleanup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discovery) sendPolls() {
|
|
||||||
for _, target := range d.pollTargets {
|
|
||||||
if err := d.sender.SendPoll(target); err != nil {
|
|
||||||
log.Printf("[->artnet] poll error: dst=%s err=%v", target.IP, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discovery) cleanup() {
|
|
||||||
d.nodesMu.Lock()
|
|
||||||
defer d.nodesMu.Unlock()
|
|
||||||
|
|
||||||
cutoff := time.Now().Add(-60 * time.Second)
|
|
||||||
for ip, node := range d.nodes {
|
|
||||||
if node.LastSeen.Before(cutoff) {
|
|
||||||
log.Printf("[artnet] node timeout ip=%s name=%s", ip, node.ShortName)
|
|
||||||
delete(d.nodes, ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandlePollReply processes an incoming ArtPollReply
|
|
||||||
func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
|
|
||||||
d.nodesMu.Lock()
|
|
||||||
defer d.nodesMu.Unlock()
|
|
||||||
|
|
||||||
ip := src.IP.String()
|
|
||||||
|
|
||||||
// Skip our own replies
|
|
||||||
localIP := net.IP(d.localIP[:])
|
|
||||||
if src.IP.Equal(localIP) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse universes from SwOut
|
|
||||||
var universes []Universe
|
|
||||||
numPorts := int(pkt.NumPortsLo)
|
|
||||||
if numPorts > 4 {
|
|
||||||
numPorts = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < numPorts; i++ {
|
|
||||||
// Check if port can output DMX
|
|
||||||
if pkt.PortTypes[i]&0x80 != 0 {
|
|
||||||
u := NewUniverse(pkt.NetSwitch, pkt.SubSwitch, pkt.SwOut[i])
|
|
||||||
universes = append(universes, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shortName := string(pkt.ShortName[:])
|
|
||||||
// Trim null bytes
|
|
||||||
for i, b := range pkt.ShortName {
|
|
||||||
if b == 0 {
|
|
||||||
shortName = string(pkt.ShortName[:i])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
longName := string(pkt.LongName[:])
|
|
||||||
for i, b := range pkt.LongName {
|
|
||||||
if b == 0 {
|
|
||||||
longName = string(pkt.LongName[:i])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node, exists := d.nodes[ip]
|
|
||||||
if !exists {
|
|
||||||
node = &Node{
|
|
||||||
IP: src.IP,
|
|
||||||
Port: pkt.Port, // Use port from packet, not UDP source port
|
|
||||||
}
|
|
||||||
d.nodes[ip] = node
|
|
||||||
}
|
|
||||||
|
|
||||||
node.ShortName = shortName
|
|
||||||
node.LongName = longName
|
|
||||||
node.LastSeen = time.Now()
|
|
||||||
node.CanTransmit = true
|
|
||||||
|
|
||||||
// Accumulate universes from multiple ArtPollReply packets
|
|
||||||
// (multi-port devices send separate replies for each group of 4 ports)
|
|
||||||
prevLen := len(node.Universes)
|
|
||||||
for _, u := range universes {
|
|
||||||
found := false
|
|
||||||
for _, existing := range node.Universes {
|
|
||||||
if existing == u {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
node.Universes = append(node.Universes, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
log.Printf("[artnet] discovered ip=%s name=%s universes=%v", ip, shortName, node.Universes)
|
|
||||||
} else if len(node.Universes) != prevLen {
|
|
||||||
log.Printf("[artnet] updated ip=%s name=%s universes=%v", ip, shortName, node.Universes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandlePoll processes an incoming ArtPoll and responds
|
|
||||||
func (d *Discovery) HandlePoll(src *net.UDPAddr) {
|
|
||||||
dst := &net.UDPAddr{IP: d.broadcast, Port: Port}
|
|
||||||
d.sendPollReplies(dst, d.inputUnivs, true)
|
|
||||||
d.sendPollReplies(dst, d.outputUnivs, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discovery) sendPollReplies(dst *net.UDPAddr, universes []Universe, isInput bool) {
|
|
||||||
groups := make(map[uint16][]Universe)
|
|
||||||
for _, u := range universes {
|
|
||||||
key := uint16(u.Net())<<8 | uint16(u.SubNet())<<4
|
|
||||||
groups[key] = append(groups[key], u)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, univs := range groups {
|
|
||||||
for i := 0; i < len(univs); i += 4 {
|
|
||||||
end := i + 4
|
|
||||||
if end > len(univs) {
|
|
||||||
end = len(univs)
|
|
||||||
}
|
|
||||||
chunk := univs[i:end]
|
|
||||||
|
|
||||||
pkt := BuildPollReplyPacket(d.localIP, d.localMAC, d.shortName, d.longName, chunk, isInput)
|
|
||||||
err := d.receiver.SendTo(pkt, dst)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[->artnet] pollreply error: dst=%s err=%v", dst.IP, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNodesForUniverse returns nodes that support a given universe
|
|
||||||
func (d *Discovery) GetNodesForUniverse(universe Universe) []*Node {
|
|
||||||
d.nodesMu.RLock()
|
|
||||||
defer d.nodesMu.RUnlock()
|
|
||||||
|
|
||||||
var result []*Node
|
|
||||||
for _, node := range d.nodes {
|
|
||||||
for _, u := range node.Universes {
|
|
||||||
if u == universe {
|
|
||||||
result = append(result, node)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result) == 0 && len(d.nodes) > 0 {
|
|
||||||
log.Printf("[artnet] no nodes for universe=%s, have %d nodes", universe, len(d.nodes))
|
|
||||||
for ip, node := range d.nodes {
|
|
||||||
log.Printf("[artnet] node ip=%s universes=%v", ip, node.Universes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllNodes returns all discovered nodes
|
|
||||||
func (d *Discovery) GetAllNodes() []*Node {
|
|
||||||
d.nodesMu.RLock()
|
|
||||||
defer d.nodesMu.RUnlock()
|
|
||||||
|
|
||||||
result := make([]*Node, 0, len(d.nodes))
|
|
||||||
for _, node := range d.nodes {
|
|
||||||
result = append(result, node)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Discovery) detectInterface() {
|
|
||||||
d.broadcast = net.IPv4bcast
|
|
||||||
|
|
||||||
ifaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, iface := range ifaces {
|
|
||||||
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := iface.Addrs()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
ipnet, ok := addr.(*net.IPNet)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ip4 := ipnet.IP.To4()
|
|
||||||
if ip4 == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(d.localIP[:], ip4)
|
|
||||||
|
|
||||||
if len(iface.HardwareAddr) == 6 {
|
|
||||||
copy(d.localMAC[:], iface.HardwareAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
bcast := make(net.IP, 4)
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
bcast[i] = ip4[i] | ^ipnet.Mask[i]
|
|
||||||
}
|
|
||||||
d.broadcast = bcast
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLocalIP sets the local IP for PollReply responses
|
|
||||||
func (d *Discovery) SetLocalIP(ip net.IP) {
|
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
|
||||||
copy(d.localIP[:], ip4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReceiver sets the receiver for sending poll replies from port 6454
|
|
||||||
func (d *Discovery) SetReceiver(r *Receiver) {
|
|
||||||
d.receiver = r
|
|
||||||
}
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
package artnet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Port = 6454
|
|
||||||
|
|
||||||
// OpCodes
|
|
||||||
OpPoll = 0x2000
|
|
||||||
OpPollReply = 0x2100
|
|
||||||
OpDmx = 0x5000
|
|
||||||
|
|
||||||
// Protocol
|
|
||||||
ProtocolVersion = 14
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ArtNetID = [8]byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00}
|
|
||||||
|
|
||||||
ErrInvalidPacket = errors.New("invalid ArtNet packet")
|
|
||||||
ErrInvalidHeader = errors.New("invalid ArtNet header")
|
|
||||||
ErrUnknownOpCode = errors.New("unknown OpCode")
|
|
||||||
ErrPacketTooShort = errors.New("packet too short")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Universe represents an ArtNet universe address (15-bit)
|
|
||||||
// Bits 14-8: Net (0-127)
|
|
||||||
// Bits 7-4: SubNet (0-15)
|
|
||||||
// Bits 3-0: Universe (0-15)
|
|
||||||
type Universe uint16
|
|
||||||
|
|
||||||
func NewUniverse(net, subnet, universe uint8) Universe {
|
|
||||||
return Universe((uint16(net&0x7F) << 8) | (uint16(subnet&0x0F) << 4) | uint16(universe&0x0F))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Universe) Net() uint8 {
|
|
||||||
return uint8((u >> 8) & 0x7F)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Universe) SubNet() uint8 {
|
|
||||||
return uint8((u >> 4) & 0x0F)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Universe) Universe() uint8 {
|
|
||||||
return uint8(u & 0x0F)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Universe) String() string {
|
|
||||||
return fmt.Sprintf("%d.%d.%d", u.Net(), u.SubNet(), u.Universe())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header is the common ArtNet packet header
|
|
||||||
type Header struct {
|
|
||||||
ID [8]byte
|
|
||||||
OpCode uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
// DMXPacket represents an ArtDmx packet (OpCode 0x5000)
|
|
||||||
type DMXPacket struct {
|
|
||||||
ProtocolVersion uint16 // High byte first
|
|
||||||
Sequence uint8 // 0x00 to disable, 0x01-0xFF sequence
|
|
||||||
Physical uint8 // Physical input port
|
|
||||||
Universe Universe // Universe address (low byte first in wire format)
|
|
||||||
Length uint16 // Data length (high byte first), 2-512, even
|
|
||||||
Data [512]byte // DMX data
|
|
||||||
}
|
|
||||||
|
|
||||||
// PollPacket represents an ArtPoll packet (OpCode 0x2000)
|
|
||||||
type PollPacket struct {
|
|
||||||
ProtocolVersion uint16
|
|
||||||
Flags uint8
|
|
||||||
DiagPriority uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
// PollReplyPacket represents an ArtPollReply packet (OpCode 0x2100)
|
|
||||||
type PollReplyPacket struct {
|
|
||||||
IPAddress [4]byte
|
|
||||||
Port uint16
|
|
||||||
VersionInfo uint16
|
|
||||||
NetSwitch uint8
|
|
||||||
SubSwitch uint8
|
|
||||||
OemHi uint8
|
|
||||||
Oem uint8
|
|
||||||
UbeaVersion uint8
|
|
||||||
Status1 uint8
|
|
||||||
EstaMan uint16
|
|
||||||
ShortName [18]byte
|
|
||||||
LongName [64]byte
|
|
||||||
NodeReport [64]byte
|
|
||||||
NumPortsHi uint8
|
|
||||||
NumPortsLo uint8
|
|
||||||
PortTypes [4]byte
|
|
||||||
GoodInput [4]byte
|
|
||||||
GoodOutput [4]byte
|
|
||||||
SwIn [4]byte
|
|
||||||
SwOut [4]byte
|
|
||||||
SwVideo uint8
|
|
||||||
SwMacro uint8
|
|
||||||
SwRemote uint8
|
|
||||||
Spare [3]byte
|
|
||||||
Style uint8
|
|
||||||
MAC [6]byte
|
|
||||||
BindIP [4]byte
|
|
||||||
BindIndex uint8
|
|
||||||
Status2 uint8
|
|
||||||
Filler [26]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParsePacket parses a raw ArtNet packet and returns the OpCode and parsed data
|
|
||||||
func ParsePacket(data []byte) (uint16, interface{}, error) {
|
|
||||||
if len(data) < 10 {
|
|
||||||
return 0, nil, ErrPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check header
|
|
||||||
if !bytes.Equal(data[:8], ArtNetID[:]) {
|
|
||||||
return 0, nil, ErrInvalidHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
opCode := binary.LittleEndian.Uint16(data[8:10])
|
|
||||||
|
|
||||||
switch opCode {
|
|
||||||
case OpDmx:
|
|
||||||
pkt, err := parseDMXPacket(data)
|
|
||||||
return opCode, pkt, err
|
|
||||||
case OpPoll:
|
|
||||||
pkt, err := parsePollPacket(data)
|
|
||||||
return opCode, pkt, err
|
|
||||||
case OpPollReply:
|
|
||||||
pkt, err := parsePollReplyPacket(data)
|
|
||||||
return opCode, pkt, err
|
|
||||||
default:
|
|
||||||
return opCode, nil, nil // Unknown but valid packet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDMXPacket(data []byte) (*DMXPacket, error) {
|
|
||||||
if len(data) < 18 {
|
|
||||||
return nil, ErrPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt := &DMXPacket{
|
|
||||||
ProtocolVersion: binary.BigEndian.Uint16(data[10:12]),
|
|
||||||
Sequence: data[12],
|
|
||||||
Physical: data[13],
|
|
||||||
Universe: Universe(binary.LittleEndian.Uint16(data[14:16])),
|
|
||||||
Length: binary.BigEndian.Uint16(data[16:18]),
|
|
||||||
}
|
|
||||||
|
|
||||||
dataLen := int(pkt.Length)
|
|
||||||
if dataLen > 512 {
|
|
||||||
dataLen = 512
|
|
||||||
}
|
|
||||||
if len(data) >= 18+dataLen {
|
|
||||||
copy(pkt.Data[:], data[18:18+dataLen])
|
|
||||||
}
|
|
||||||
|
|
||||||
return pkt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePollPacket(data []byte) (*PollPacket, error) {
|
|
||||||
if len(data) < 14 {
|
|
||||||
return nil, ErrPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PollPacket{
|
|
||||||
ProtocolVersion: binary.BigEndian.Uint16(data[10:12]),
|
|
||||||
Flags: data[12],
|
|
||||||
DiagPriority: data[13],
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePollReplyPacket(data []byte) (*PollReplyPacket, error) {
|
|
||||||
if len(data) < 207 {
|
|
||||||
return nil, ErrPacketTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt := &PollReplyPacket{
|
|
||||||
Port: binary.LittleEndian.Uint16(data[14:16]),
|
|
||||||
VersionInfo: binary.BigEndian.Uint16(data[16:18]),
|
|
||||||
NetSwitch: data[18],
|
|
||||||
SubSwitch: data[19],
|
|
||||||
OemHi: data[20],
|
|
||||||
Oem: data[21],
|
|
||||||
UbeaVersion: data[22],
|
|
||||||
Status1: data[23],
|
|
||||||
EstaMan: binary.LittleEndian.Uint16(data[24:26]),
|
|
||||||
NumPortsHi: data[172],
|
|
||||||
NumPortsLo: data[173],
|
|
||||||
Style: data[200],
|
|
||||||
BindIndex: data[212],
|
|
||||||
Status2: data[213],
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(pkt.IPAddress[:], data[10:14])
|
|
||||||
copy(pkt.ShortName[:], data[26:44])
|
|
||||||
copy(pkt.LongName[:], data[44:108])
|
|
||||||
copy(pkt.NodeReport[:], data[108:172])
|
|
||||||
copy(pkt.PortTypes[:], data[174:178])
|
|
||||||
copy(pkt.GoodInput[:], data[178:182])
|
|
||||||
copy(pkt.GoodOutput[:], data[182:186])
|
|
||||||
copy(pkt.SwIn[:], data[186:190])
|
|
||||||
copy(pkt.SwOut[:], data[190:194])
|
|
||||||
copy(pkt.MAC[:], data[201:207])
|
|
||||||
copy(pkt.BindIP[:], data[207:211])
|
|
||||||
|
|
||||||
return pkt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildDMXPacket creates a raw ArtDmx packet
|
|
||||||
func BuildDMXPacket(universe Universe, sequence uint8, data []byte) []byte {
|
|
||||||
dataLen := len(data)
|
|
||||||
if dataLen > 512 {
|
|
||||||
dataLen = 512
|
|
||||||
}
|
|
||||||
// Length must be even
|
|
||||||
if dataLen%2 != 0 {
|
|
||||||
dataLen++
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 18+dataLen)
|
|
||||||
|
|
||||||
// Header
|
|
||||||
copy(buf[0:8], ArtNetID[:])
|
|
||||||
binary.LittleEndian.PutUint16(buf[8:10], OpDmx)
|
|
||||||
|
|
||||||
// DMX packet fields
|
|
||||||
binary.BigEndian.PutUint16(buf[10:12], ProtocolVersion)
|
|
||||||
buf[12] = sequence
|
|
||||||
buf[13] = 0 // Physical
|
|
||||||
binary.LittleEndian.PutUint16(buf[14:16], uint16(universe))
|
|
||||||
binary.BigEndian.PutUint16(buf[16:18], uint16(dataLen))
|
|
||||||
copy(buf[18:], data[:dataLen])
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildPollPacket creates an ArtPoll packet
|
|
||||||
func BuildPollPacket() []byte {
|
|
||||||
buf := make([]byte, 14)
|
|
||||||
|
|
||||||
copy(buf[0:8], ArtNetID[:])
|
|
||||||
binary.LittleEndian.PutUint16(buf[8:10], OpPoll)
|
|
||||||
binary.BigEndian.PutUint16(buf[10:12], ProtocolVersion)
|
|
||||||
buf[12] = 0x00 // Flags
|
|
||||||
buf[13] = 0x00 // DiagPriority
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildPollReplyPacket creates an ArtPollReply packet
|
|
||||||
// isInput: true = we transmit to network (SwIn), false = we receive from network (SwOut)
|
|
||||||
func BuildPollReplyPacket(ip [4]byte, mac [6]byte, shortName, longName string, universes []Universe, isInput bool) []byte {
|
|
||||||
buf := make([]byte, 240)
|
|
||||||
|
|
||||||
copy(buf[0:8], ArtNetID[:])
|
|
||||||
binary.LittleEndian.PutUint16(buf[8:10], OpPollReply)
|
|
||||||
copy(buf[10:14], ip[:])
|
|
||||||
binary.LittleEndian.PutUint16(buf[14:16], Port)
|
|
||||||
binary.BigEndian.PutUint16(buf[16:18], ProtocolVersion)
|
|
||||||
|
|
||||||
if len(universes) > 0 {
|
|
||||||
buf[18] = universes[0].Net()
|
|
||||||
buf[19] = universes[0].SubNet()
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(buf[26:44], shortName)
|
|
||||||
copy(buf[44:108], longName)
|
|
||||||
|
|
||||||
numPorts := len(universes)
|
|
||||||
if numPorts > 4 {
|
|
||||||
numPorts = 4
|
|
||||||
}
|
|
||||||
buf[173] = byte(numPorts)
|
|
||||||
|
|
||||||
for i := 0; i < numPorts; i++ {
|
|
||||||
if isInput {
|
|
||||||
buf[174+i] = 0x40
|
|
||||||
buf[178+i] = 0x80
|
|
||||||
buf[186+i] = universes[i].Universe()
|
|
||||||
} else {
|
|
||||||
buf[174+i] = 0x80
|
|
||||||
buf[182+i] = 0x80
|
|
||||||
buf[190+i] = universes[i].Universe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(buf[201:207], mac[:])
|
|
||||||
copy(buf[207:211], ip[:])
|
|
||||||
buf[211] = 1
|
|
||||||
buf[212] = 0x08
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
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(addr *net.UDPAddr, handler PacketHandler) (*Receiver, error) {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendTo sends a raw packet through the receiver's socket (port 6454)
|
|
||||||
func (r *Receiver) SendTo(data []byte, addr *net.UDPAddr) error {
|
|
||||||
_, err := r.conn.WriteToUDP(data, addr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package artnet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sender sends ArtNet packets
|
|
||||||
type Sender struct {
|
|
||||||
conn *net.UDPConn
|
|
||||||
sequences map[Universe]uint8
|
|
||||||
seqMu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSender creates a new ArtNet sender
|
|
||||||
func NewSender() (*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
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Sender{
|
|
||||||
conn: conn,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendPoll sends an ArtPoll packet to the specified address
|
|
||||||
func (s *Sender) SendPoll(addr *net.UDPAddr) error {
|
|
||||||
pkt := BuildPollPacket()
|
|
||||||
_, err := s.conn.WriteToUDP(pkt, addr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the sender
|
|
||||||
func (s *Sender) Close() error {
|
|
||||||
return s.conn.Close()
|
|
||||||
}
|
|
||||||
2
main.go
2
main.go
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/gopatchy/artmap/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/sacn"
|
"github.com/gopatchy/artmap/sacn"
|
||||||
|
|||||||
Reference in New Issue
Block a user