add basic start on artnet decoding

This commit is contained in:
Joel Wetzell
2025-12-22 20:09:02 -06:00
commit 307afe8bed
7 changed files with 236 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Art-Net
Pure GoLang implementation of the [Art-Net protocol](https://art-net.org.uk). View the [specification here](https://art-net.org.uk/downloads/art-net.pdf).
+62
View File
@@ -0,0 +1,62 @@
package artnet
import "fmt"
var (
OpPoll uint16 = 0x2000
OpPollReply uint16 = 0x2100
OpDiagData uint16 = 0x2300
OpCommand uint16 = 0x2400
OpDataRequest uint16 = 0x2700
OpDataReply uint16 = 0x2800
OpOutput uint16 = 0x5000
OpDmx uint16 = 0x5000
OpNzs uint16 = 0x5100
OpSync uint16 = 0x5200
OpAddress uint16 = 0x6000
OpInput uint16 = 0x7000
OpTodRequest uint16 = 0x8000
OpTodData uint16 = 0x8100
OpTodControl uint16 = 0x8200
OpRdm uint16 = 0x8300
OpRdmSub uint16 = 0x8400
OpVideoSetup uint16 = 0xa010
OpVideoPalette uint16 = 0xa020
OpVideoData uint16 = 0xa040
OpMacMaster uint16 = 0xf000
OpMacSlave uint16 = 0xf100
OpFirmwareMaster uint16 = 0xf200
OpFirmwareReply uint16 = 0xf300
OpFileTnMaster uint16 = 0xf400
OpFileFnMaster uint16 = 0xf500
OpFileFnReply uint16 = 0xf600
OpIpProg uint16 = 0xf800
OpIpProgReply uint16 = 0xf900
OpMedia uint16 = 0x9000
OpMediaPatch uint16 = 0x9100
OpMediaControl uint16 = 0x9200
OpMediaContrlReply uint16 = 0x9300
OpTimeCode uint16 = 0x9700
OpTimeSync uint16 = 0x9800
OpTrigger uint16 = 0x9900
OpDirectory uint16 = 0x9a00
OpDirectoryReply uint16 = 0x9b00
)
func Decode(bytes []byte) (ArtNetPacket, error) {
header, err := NewHeader(bytes)
if err != nil {
return nil, err
}
switch header.OpCode {
case OpPoll:
return NewArtPoll(header, bytes)
case OpCommand:
return NewArtCommand(header, bytes)
case OpDmx:
return NewArtDmx(header, bytes)
default:
return nil, fmt.Errorf("unhandled opcode: %#x", header.OpCode)
}
}
+43
View File
@@ -0,0 +1,43 @@
package artnet
import (
"errors"
)
type ArtCommand struct {
Header *ArtNetHeader
EstaManHi uint8
EstaManLo uint8
Length uint16
Data string
}
func NewArtCommand(header *ArtNetHeader, data []byte) (*ArtCommand, error) {
artCommand := ArtCommand{
Header: header,
}
if len(data) < 18 {
return nil, errors.New("ArtCommand packet must be at least 14 bytes long")
}
offset := 12
artCommand.EstaManHi = data[offset]
artCommand.EstaManLo = data[offset+1]
artCommand.Length = uint16(data[offset+2])<<8 + uint16(data[offset+3])
commandDataOffset := offset + 4
if len(data[commandDataOffset:]) < int(artCommand.Length) {
return nil, errors.New("ArtCommand packet length mismatch")
}
artCommand.Data = string(data[commandDataOffset : commandDataOffset+int(artCommand.Length)-1])
return &artCommand, nil
}
func (ap *ArtCommand) GetOpCode() uint16 {
return ap.Header.OpCode
}
+49
View File
@@ -0,0 +1,49 @@
package artnet
import (
"errors"
)
type ArtDmx struct {
Header *ArtNetHeader
Sequence uint8
Physical uint8
SubUni uint8
Net uint8
Length uint16
Data []uint8
}
func NewArtDmx(header *ArtNetHeader, data []byte) (*ArtDmx, error) {
artDmx := ArtDmx{
Header: header,
}
if len(data) < 18 {
return nil, errors.New("ArtDmx packet must be at least 14 bytes long")
}
offset := 12
artDmx.Sequence = data[offset]
artDmx.Physical = data[offset+1]
artDmx.SubUni = data[offset+2]
artDmx.Net = data[offset+3]
artDmx.Length = uint16(data[offset+4])<<8 + uint16(data[offset+5])
dmxDataOffset := offset + 6
if len(data[dmxDataOffset:]) < int(artDmx.Length) {
return nil, errors.New("ArtDmx packet length mismatch")
}
artDmx.Data = make([]uint8, artDmx.Length)
copy(artDmx.Data, data[dmxDataOffset:dmxDataOffset+int(artDmx.Length)])
return &artDmx, nil
}
func (ap *ArtDmx) GetOpCode() uint16 {
return ap.Header.OpCode
}
+3
View File
@@ -0,0 +1,3 @@
module github.com/jwetzell/artnet-go
go 1.23.1
+37
View File
@@ -0,0 +1,37 @@
package artnet
import (
"encoding/binary"
"errors"
"slices"
)
type ArtNetPacket interface {
GetOpCode() uint16
}
var ArtNetID []uint8 = []uint8{'A', 'r', 't', '-', 'N', 'e', 't', 0x00}
type ArtNetHeader struct {
ID []uint8
OpCode uint16
ProtVerHi uint8
ProtVerLo uint8
}
func NewHeader(data []byte) (*ArtNetHeader, error) {
if len(data) < 12 {
return nil, errors.New("header must be at least 12 bytes")
}
if !slices.Equal(ArtNetID, data[0:8]) {
return nil, errors.New("header id does not match Art-Net ID")
}
return &ArtNetHeader{
OpCode: binary.LittleEndian.Uint16(data[8:10]),
ProtVerHi: data[10],
ProtVerLo: data[11],
}, nil
}
+40
View File
@@ -0,0 +1,40 @@
package artnet
import (
"errors"
)
type ArtPoll struct {
Header *ArtNetHeader
Flags uint8
DiagPriority uint8
AddressTopHi uint8
AddressTopLo uint8
AddressBottomHi uint8
AddressBottomLo uint8
EstaManHi uint8
EstaManLo uint8
OemHi uint8
OemLo uint8
}
func NewArtPoll(header *ArtNetHeader, data []byte) (*ArtPoll, error) {
artPoll := ArtPoll{
Header: header,
}
if len(data) < 14 {
return nil, errors.New("ArtPoll packet must be at least 14 bytes long")
}
offset := 12
artPoll.Flags = data[offset]
artPoll.DiagPriority = data[offset+1]
//TODO(jwetzell): unpack extended poll fields
return &artPoll, nil
}
func (ap *ArtPoll) GetOpCode() uint16 {
return ap.Header.OpCode
}