Initial Art-Net protocol library
This commit is contained in:
261
discovery.go
Normal file
261
discovery.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package artnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
IP net.IP
|
||||||
|
Port uint16
|
||||||
|
MAC net.HardwareAddr
|
||||||
|
ShortName string
|
||||||
|
LongName string
|
||||||
|
Inputs []Universe
|
||||||
|
Outputs []Universe
|
||||||
|
LastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Discovery struct {
|
||||||
|
sender *Sender
|
||||||
|
receiver *Receiver
|
||||||
|
nodes map[string]*Node
|
||||||
|
nodesMu sync.RWMutex
|
||||||
|
localIP [4]byte
|
||||||
|
localMAC [6]byte
|
||||||
|
broadcast net.IP
|
||||||
|
shortName string
|
||||||
|
longName string
|
||||||
|
inputUnivs []Universe
|
||||||
|
outputUnivs []Universe
|
||||||
|
pollTargets []*net.UDPAddr
|
||||||
|
done chan struct{}
|
||||||
|
onChange func(*Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDiscovery(sender *Sender, shortName, longName string, inputUnivs, outputUnivs []Universe, pollTargets []*net.UDPAddr) *Discovery {
|
||||||
|
return &Discovery{
|
||||||
|
sender: sender,
|
||||||
|
nodes: map[string]*Node{},
|
||||||
|
shortName: shortName,
|
||||||
|
longName: longName,
|
||||||
|
inputUnivs: inputUnivs,
|
||||||
|
outputUnivs: outputUnivs,
|
||||||
|
pollTargets: pollTargets,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Discovery) Start() {
|
||||||
|
d.detectInterface()
|
||||||
|
go d.pollLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Discovery) Stop() {
|
||||||
|
close(d.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Discovery) SetReceiver(r *Receiver) {
|
||||||
|
d.receiver = r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Discovery) SetOnChange(fn func(*Node)) {
|
||||||
|
d.onChange = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Discovery) pollLoop() {
|
||||||
|
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 {
|
||||||
|
d.sender.SendPoll(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
delete(d.nodes, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
|
||||||
|
d.nodesMu.Lock()
|
||||||
|
defer d.nodesMu.Unlock()
|
||||||
|
|
||||||
|
ip := src.IP.String()
|
||||||
|
|
||||||
|
localIP := net.IP(d.localIP[:])
|
||||||
|
if src.IP.Equal(localIP) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, exists := d.nodes[ip]
|
||||||
|
if !exists {
|
||||||
|
node = &Node{
|
||||||
|
IP: src.IP,
|
||||||
|
Port: pkt.Port,
|
||||||
|
}
|
||||||
|
d.nodes[ip] = node
|
||||||
|
}
|
||||||
|
|
||||||
|
node.ShortName = pkt.GetShortName()
|
||||||
|
node.LongName = pkt.GetLongName()
|
||||||
|
node.MAC = pkt.MACAddr()
|
||||||
|
node.LastSeen = time.Now()
|
||||||
|
|
||||||
|
for _, u := range pkt.InputUniverses() {
|
||||||
|
if !containsUniverse(node.Inputs, u) {
|
||||||
|
node.Inputs = append(node.Inputs, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, u := range pkt.OutputUniverses() {
|
||||||
|
if !containsUniverse(node.Outputs, u) {
|
||||||
|
node.Outputs = append(node.Outputs, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.onChange != nil {
|
||||||
|
d.onChange(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Discovery) HandlePoll(src *net.UDPAddr) {
|
||||||
|
if d.receiver == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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 := 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)
|
||||||
|
d.receiver.SendTo(pkt, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Outputs {
|
||||||
|
if u == universe {
|
||||||
|
result = append(result, node)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
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) SetLocalIP(ip net.IP) {
|
||||||
|
if ip4 := ip.To4(); ip4 != nil {
|
||||||
|
copy(d.localIP[:], ip4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsUniverse(slice []Universe, val Universe) bool {
|
||||||
|
for _, v := range slice {
|
||||||
|
if v == val {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
104
poller.go
Normal file
104
poller.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package artnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Poller struct {
|
||||||
|
sender *Sender
|
||||||
|
targets []*net.UDPAddr
|
||||||
|
interval time.Duration
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPoller(sender *Sender, targets []*net.UDPAddr, interval time.Duration) *Poller {
|
||||||
|
return &Poller{
|
||||||
|
sender: sender,
|
||||||
|
targets: targets,
|
||||||
|
interval: interval,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBroadcastPoller(sender *Sender, interval time.Duration) *Poller {
|
||||||
|
return &Poller{
|
||||||
|
sender: sender,
|
||||||
|
targets: []*net.UDPAddr{{IP: net.IPv4bcast, Port: Port}},
|
||||||
|
interval: interval,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) Start() {
|
||||||
|
go p.loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) Stop() {
|
||||||
|
close(p.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) Poll() {
|
||||||
|
for _, target := range p.targets {
|
||||||
|
p.sender.SendPoll(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) loop() {
|
||||||
|
p.Poll()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(p.interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.done:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
p.Poll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) SetTargets(targets []*net.UDPAddr) {
|
||||||
|
p.targets = targets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Poller) AddTarget(target *net.UDPAddr) {
|
||||||
|
p.targets = append(p.targets, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BroadcastAddr() *net.UDPAddr {
|
||||||
|
return &net.UDPAddr{IP: net.IPv4bcast, Port: Port}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnicastAddr(ip net.IP) *net.UDPAddr {
|
||||||
|
return &net.UDPAddr{IP: ip, Port: Port}
|
||||||
|
}
|
||||||
|
|
||||||
|
func InterfaceBroadcast(iface net.Interface) *net.UDPAddr {
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return BroadcastAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipnet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip4 := ipnet.IP.To4()
|
||||||
|
if ip4 == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bcast := make(net.IP, 4)
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
bcast[i] = ip4[i] | ^ipnet.Mask[i]
|
||||||
|
}
|
||||||
|
return &net.UDPAddr{IP: bcast, Port: Port}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BroadcastAddr()
|
||||||
|
}
|
||||||
322
protocol.go
Normal file
322
protocol.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
package artnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Port = 6454
|
||||||
|
ProtocolVersion = 14
|
||||||
|
|
||||||
|
OpPoll uint16 = 0x2000
|
||||||
|
OpPollReply uint16 = 0x2100
|
||||||
|
OpDmx uint16 = 0x5000
|
||||||
|
OpSync uint16 = 0x5200
|
||||||
|
OpAddress uint16 = 0x6000
|
||||||
|
OpInput uint16 = 0x7000
|
||||||
|
OpTodData uint16 = 0x8100
|
||||||
|
OpTodControl uint16 = 0x8200
|
||||||
|
OpRdm uint16 = 0x8300
|
||||||
|
|
||||||
|
PortTypeOutput uint8 = 0x80
|
||||||
|
PortTypeInput uint8 = 0x40
|
||||||
|
|
||||||
|
GoodOutputDataTransmitted uint8 = 0x80
|
||||||
|
GoodInputDataReceived uint8 = 0x80
|
||||||
|
|
||||||
|
StyleNode uint8 = 0x00
|
||||||
|
StyleController uint8 = 0x01
|
||||||
|
StyleMedia uint8 = 0x02
|
||||||
|
StyleRoute uint8 = 0x03
|
||||||
|
StyleBackup uint8 = 0x04
|
||||||
|
StyleConfig uint8 = 0x05
|
||||||
|
StyleVisual uint8 = 0x06
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ID = [8]byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00}
|
||||||
|
|
||||||
|
ErrInvalidHeader = errors.New("invalid Art-Net header")
|
||||||
|
ErrPacketTooShort = errors.New("packet too short")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Universe uint16
|
||||||
|
|
||||||
|
func NewUniverse(netVal, subnet, universe uint8) Universe {
|
||||||
|
return Universe((uint16(netVal&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()) }
|
||||||
|
|
||||||
|
type DMXPacket struct {
|
||||||
|
ProtocolVersion uint16
|
||||||
|
Sequence uint8
|
||||||
|
Physical uint8
|
||||||
|
Universe Universe
|
||||||
|
Length uint16
|
||||||
|
Data [512]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollPacket struct {
|
||||||
|
ProtocolVersion uint16
|
||||||
|
Flags uint8
|
||||||
|
DiagPriority uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
Style uint8
|
||||||
|
MAC [6]byte
|
||||||
|
BindIP [4]byte
|
||||||
|
BindIndex uint8
|
||||||
|
Status2 uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PollReplyPacket) IP() net.IP {
|
||||||
|
return net.IPv4(p.IPAddress[0], p.IPAddress[1], p.IPAddress[2], p.IPAddress[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PollReplyPacket) MACAddr() net.HardwareAddr {
|
||||||
|
return net.HardwareAddr(p.MAC[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PollReplyPacket) GetShortName() string {
|
||||||
|
return strings.TrimRight(string(p.ShortName[:]), "\x00")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PollReplyPacket) GetLongName() string {
|
||||||
|
return strings.TrimRight(string(p.LongName[:]), "\x00")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PollReplyPacket) NumPorts() int {
|
||||||
|
n := int(p.NumPortsLo)
|
||||||
|
if n > 4 {
|
||||||
|
n = 4
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PollReplyPacket) InputUniverses() []Universe {
|
||||||
|
var result []Universe
|
||||||
|
for i := 0; i < p.NumPorts(); i++ {
|
||||||
|
if p.PortTypes[i]&PortTypeInput != 0 {
|
||||||
|
u := NewUniverse(p.NetSwitch, p.SubSwitch, p.SwIn[i])
|
||||||
|
result = append(result, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PollReplyPacket) OutputUniverses() []Universe {
|
||||||
|
var result []Universe
|
||||||
|
for i := 0; i < p.NumPorts(); i++ {
|
||||||
|
if p.PortTypes[i]&PortTypeOutput != 0 {
|
||||||
|
u := NewUniverse(p.NetSwitch, p.SubSwitch, p.SwOut[i])
|
||||||
|
result = append(result, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParsePacket(data []byte) (uint16, interface{}, error) {
|
||||||
|
if len(data) < 10 {
|
||||||
|
return 0, nil, ErrPacketTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(data[:8], ID[:]) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildDMXPacket(universe Universe, sequence uint8, data []byte) []byte {
|
||||||
|
dataLen := len(data)
|
||||||
|
if dataLen > 512 {
|
||||||
|
dataLen = 512
|
||||||
|
}
|
||||||
|
if dataLen%2 != 0 {
|
||||||
|
dataLen++
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 18+dataLen)
|
||||||
|
copy(buf[0:8], ID[:])
|
||||||
|
binary.LittleEndian.PutUint16(buf[8:10], OpDmx)
|
||||||
|
binary.BigEndian.PutUint16(buf[10:12], ProtocolVersion)
|
||||||
|
buf[12] = sequence
|
||||||
|
buf[13] = 0
|
||||||
|
binary.LittleEndian.PutUint16(buf[14:16], uint16(universe))
|
||||||
|
binary.BigEndian.PutUint16(buf[16:18], uint16(dataLen))
|
||||||
|
copy(buf[18:], data[:dataLen])
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildPollPacket() []byte {
|
||||||
|
buf := make([]byte, 14)
|
||||||
|
copy(buf[0:8], ID[:])
|
||||||
|
binary.LittleEndian.PutUint16(buf[8:10], OpPoll)
|
||||||
|
binary.BigEndian.PutUint16(buf[10:12], ProtocolVersion)
|
||||||
|
buf[12] = 0x00
|
||||||
|
buf[13] = 0x00
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildPollReplyPacket(ip [4]byte, mac [6]byte, shortName, longName string, universes []Universe, isInput bool) []byte {
|
||||||
|
buf := make([]byte, 240)
|
||||||
|
copy(buf[0:8], ID[:])
|
||||||
|
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] = PortTypeInput
|
||||||
|
buf[178+i] = GoodInputDataReceived
|
||||||
|
buf[186+i] = universes[i].Universe()
|
||||||
|
} else {
|
||||||
|
buf[174+i] = PortTypeOutput
|
||||||
|
buf[182+i] = GoodOutputDataTransmitted
|
||||||
|
buf[190+i] = universes[i].Universe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(buf[201:207], mac[:])
|
||||||
|
copy(buf[207:211], ip[:])
|
||||||
|
buf[211] = 1
|
||||||
|
buf[212] = 0x08
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
107
receiver.go
Normal file
107
receiver.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package artnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
HandleDMX(src *net.UDPAddr, pkt *DMXPacket)
|
||||||
|
HandlePoll(src *net.UDPAddr, pkt *PollPacket)
|
||||||
|
HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Receiver struct {
|
||||||
|
conn *net.UDPConn
|
||||||
|
handler Handler
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReceiver(addr *net.UDPAddr, handler Handler) (*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
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultReceiver(handler Handler) (*Receiver, error) {
|
||||||
|
return NewReceiver(&net.UDPAddr{Port: Port}, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Receiver) Start() {
|
||||||
|
go r.loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Receiver) Stop() {
|
||||||
|
close(r.done)
|
||||||
|
r.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Receiver) Conn() *net.UDPConn {
|
||||||
|
return r.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Receiver) LocalAddr() net.Addr {
|
||||||
|
return r.conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Receiver) SendTo(data []byte, addr *net.UDPAddr) error {
|
||||||
|
_, err := r.conn.WriteToUDP(data, addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Receiver) loop() {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
r.conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||||
|
n, src, err := r.conn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-r.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.handle(src, buf[:n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Receiver) handle(src *net.UDPAddr, data []byte) {
|
||||||
|
opCode, pkt, err := ParsePacket(data)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
sender.go
Normal file
71
sender.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package artnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sender struct {
|
||||||
|
conn *net.UDPConn
|
||||||
|
sequences map[Universe]uint8
|
||||||
|
seqMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSender() (*Sender, error) {
|
||||||
|
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Sender{
|
||||||
|
conn: conn,
|
||||||
|
sequences: map[Universe]uint8{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSenderFromConn(conn *net.UDPConn) *Sender {
|
||||||
|
return &Sender{
|
||||||
|
conn: conn,
|
||||||
|
sequences: map[Universe]uint8{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
s.sequences[universe] = seq
|
||||||
|
s.seqMu.Unlock()
|
||||||
|
|
||||||
|
pkt := BuildDMXPacket(universe, seq, data)
|
||||||
|
_, err := s.conn.WriteToUDP(pkt, addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sender) SendPoll(addr *net.UDPAddr) error {
|
||||||
|
pkt := BuildPollPacket()
|
||||||
|
_, err := s.conn.WriteToUDP(pkt, addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sender) SendPollReply(addr *net.UDPAddr, localIP [4]byte, localMAC [6]byte, shortName, longName string, universes []Universe, isInput bool) error {
|
||||||
|
pkt := BuildPollReplyPacket(localIP, localMAC, shortName, longName, universes, isInput)
|
||||||
|
_, err := s.conn.WriteToUDP(pkt, addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sender) SendRaw(addr *net.UDPAddr, data []byte) error {
|
||||||
|
_, err := s.conn.WriteToUDP(data, addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sender) Close() error {
|
||||||
|
return s.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sender) LocalAddr() net.Addr {
|
||||||
|
return s.conn.LocalAddr()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user