From e75a4524bb827991f958a86a6671308bb214ee1a Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sat, 31 Jan 2026 15:54:36 -0600 Subject: [PATCH] add support for timecode packets --- artnet.go | 2 ++ timecode.go | 79 ++++++++++++++++++++++++++++++++++++++++++++ timecode_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 timecode.go create mode 100644 timecode_test.go diff --git a/artnet.go b/artnet.go index a7fb4f7..5c3ad9b 100644 --- a/artnet.go +++ b/artnet.go @@ -59,6 +59,8 @@ func Decode(bytes []byte) (ArtNetPacket, error) { return NewArtPoll(bytes) case OpDmx: return NewArtDmx(bytes) + case OpTimeCode: + return NewArtTimeCode(bytes) default: return nil, fmt.Errorf("unhandled opcode: %#x", opCode) } diff --git a/timecode.go b/timecode.go new file mode 100644 index 0000000..5e8ca1f --- /dev/null +++ b/timecode.go @@ -0,0 +1,79 @@ +package artnet + +import ( + "encoding/binary" + "errors" + "fmt" + "slices" +) + +type ArtTimeCode struct { + ID []uint8 + OpCode uint16 + ProtVerHi uint8 + ProtVerLo uint8 + Filler1 uint8 + StreamId uint8 + Frames uint8 + Seconds uint8 + Minutes uint8 + Hours uint8 + Type uint8 +} + +func NewArtTimeCode(data []byte) (*ArtTimeCode, error) { + artTimeCode := ArtTimeCode{} + + err := artTimeCode.UnmarshalBinary(data) + + if err != nil { + return nil, err + } + + return &artTimeCode, nil +} + +func (atc *ArtTimeCode) GetOpCode() uint16 { + return atc.OpCode +} + +func (atc *ArtTimeCode) GetProtVer() uint16 { + return uint16(atc.ProtVerHi)<<8 + uint16(atc.ProtVerLo) +} + +func (atc *ArtTimeCode) GetID() []uint8 { + return atc.ID +} + +func (atc *ArtTimeCode) UnmarshalBinary(data []byte) error { + fmt.Println(data) + if len(data) < 14 { + return errors.New("ArtTimeCode packet must be at least 14 bytes long") + } + + atc.ID = data[0:8] + + if !slices.Equal(ArtNetID, atc.ID) { + return errors.New("ID does not match Art-Net ID") + } + + atc.OpCode = binary.LittleEndian.Uint16(data[8:10]) + atc.ProtVerHi = data[10] + atc.ProtVerLo = data[11] + + offset := 12 + atc.Filler1 = data[offset] + atc.StreamId = data[offset+1] + atc.Frames = data[offset+2] + atc.Seconds = data[offset+3] + atc.Minutes = data[offset+4] + atc.Hours = data[offset+5] + atc.Type = data[offset+6] + return nil +} + +func (atc *ArtTimeCode) MarshalBinary() ([]byte, error) { + data := []byte(ArtNetID) + data = append(data, byte(atc.OpCode), byte(atc.OpCode>>8), atc.ProtVerHi, atc.ProtVerLo, atc.Filler1, atc.StreamId, atc.Frames, atc.Seconds, atc.Minutes, atc.Hours, atc.Type) + return data, nil +} diff --git a/timecode_test.go b/timecode_test.go new file mode 100644 index 0000000..7c41bde --- /dev/null +++ b/timecode_test.go @@ -0,0 +1,85 @@ +package artnet_test + +import ( + "slices" + "testing" + + "github.com/jwetzell/artnet-go" +) + +func TestGoodArtTimeCode(t *testing.T) { + tests := []struct { + Data []byte + Expected *artnet.ArtTimeCode + }{ + { + Data: []byte{65, 114, 116, 45, 78, 101, 116, 0, 0, 151, 0, 14, 0, 0, 11, 17, 3, 0, 0}, + Expected: &artnet.ArtTimeCode{ + ID: []byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00}, + OpCode: artnet.OpTimeCode, + ProtVerHi: 0, + ProtVerLo: 14, + Filler1: 0, + StreamId: 0, + Frames: 11, + Seconds: 17, + Minutes: 3, + Hours: 0, + Type: 0, + }, + }, + } + + for _, test := range tests { + got, err := artnet.NewArtTimeCode(test.Data) + + if err != nil { + t.Fatalf("failed to decode ArtTimeCode: %s", err) + } + + if got.OpCode != test.Expected.OpCode { + t.Fatalf("ArtTimeCode OpCode does not match got: %d expected: %d", got.OpCode, test.Expected.OpCode) + } + + if !slices.Equal(got.ID, test.Expected.ID) { + t.Fatalf("ArtTimeCode ID does not match got: %+v expected: %+v", got.ID, test.Expected.ID) + } + + if got.Filler1 != test.Expected.Filler1 { + t.Fatalf("ArtTimeCode Filler1 does not match got: %d expected: %d", got.Filler1, test.Expected.Filler1) + } + + if got.StreamId != test.Expected.StreamId { + t.Fatalf("ArtTimeCode StreamId does not match got: %d expected: %d", got.StreamId, test.Expected.StreamId) + } + + if got.Frames != test.Expected.Frames { + t.Fatalf("ArtTimeCode Frames does not match got: %d expected: %d", got.Frames, test.Expected.Frames) + } + + if got.Seconds != test.Expected.Seconds { + t.Fatalf("ArtTimeCode Seconds does not match got: %d expected: %d", got.Seconds, test.Expected.Seconds) + } + + if got.Minutes != test.Expected.Minutes { + t.Fatalf("ArtTimeCode Minutes does not match got: %d expected: %d", got.Minutes, test.Expected.Minutes) + } + + if got.Hours != test.Expected.Hours { + t.Fatalf("ArtTimeCode Hours does not match got: %d expected: %d", got.Hours, test.Expected.Hours) + } + + if got.Type != test.Expected.Type { + t.Fatalf("ArtTimeCode Type does not match got: %d expected: %d", got.Type, test.Expected.Type) + } + + if got.ProtVerHi != test.Expected.ProtVerHi { + t.Fatalf("ArtTimeCode ProtVerHi does not match got: %d expected: %d", got.ProtVerHi, test.Expected.ProtVerHi) + } + + if got.ProtVerLo != test.Expected.ProtVerLo { + t.Fatalf("ArtTimeCode ProtVerLo does not match got: %d expected: %d", got.ProtVerLo, test.Expected.ProtVerLo) + } + + } +}