From 307afe8bedd9b159cddd158e56efd8641224e9f9 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 22 Dec 2025 20:09:02 -0600 Subject: [PATCH] add basic start on artnet decoding --- README.md | 2 ++ artnet.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ command.go | 43 +++++++++++++++++++++++++++++++++++++ dmx.go | 49 ++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 +++ packet.go | 37 ++++++++++++++++++++++++++++++++ poll.go | 40 +++++++++++++++++++++++++++++++++++ 7 files changed, 236 insertions(+) create mode 100644 README.md create mode 100644 artnet.go create mode 100644 command.go create mode 100644 dmx.go create mode 100644 go.mod create mode 100644 packet.go create mode 100644 poll.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..945c459 --- /dev/null +++ b/README.md @@ -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). \ No newline at end of file diff --git a/artnet.go b/artnet.go new file mode 100644 index 0000000..1d94b41 --- /dev/null +++ b/artnet.go @@ -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) + + } +} diff --git a/command.go b/command.go new file mode 100644 index 0000000..f538cc6 --- /dev/null +++ b/command.go @@ -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 +} diff --git a/dmx.go b/dmx.go new file mode 100644 index 0000000..33b52c7 --- /dev/null +++ b/dmx.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b36e665 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/jwetzell/artnet-go + +go 1.23.1 diff --git a/packet.go b/packet.go new file mode 100644 index 0000000..1e18c32 --- /dev/null +++ b/packet.go @@ -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 + +} diff --git a/poll.go b/poll.go new file mode 100644 index 0000000..dd3636a --- /dev/null +++ b/poll.go @@ -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 +}