From 888ab65595554a91e6a2ee461b72affc3708c5f5 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Thu, 26 Dec 2024 17:40:52 -0600 Subject: [PATCH] initial commit --- README.md | 4 ++ ddp.go | 184 +++++++++++++++++++++++++++++++++++++++++++++++ ddp_test.go | 169 +++++++++++++++++++++++++++++++++++++++++++ examples/wled.go | 49 +++++++++++++ go.mod | 3 + 5 files changed, 409 insertions(+) create mode 100644 README.md create mode 100644 ddp.go create mode 100644 ddp_test.go create mode 100644 examples/wled.go create mode 100644 go.mod diff --git a/README.md b/README.md new file mode 100644 index 0000000..53f548d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# ddp-go +Collection of [DDP](http://www.3waylabs.com/ddp/) things written in Go + +- [DDP encoding/decoding library](./ddp.go) \ No newline at end of file diff --git a/ddp.go b/ddp.go new file mode 100644 index 0000000..d2c46d5 --- /dev/null +++ b/ddp.go @@ -0,0 +1,184 @@ +package ddp + +import ( + "encoding/binary" + "errors" +) + +type DDPDataType struct { + Standard bool + Type uint8 + BitsPerPixel uint8 +} +type DDPHeaderFlags struct { + Version uint8 + Timecode bool + Storage bool + Reply bool + Query bool + Push bool +} + +type DDPTimecode struct { + Seconds uint16 + FractionalSeconds uint16 +} + +type DDPHeader struct { + Timecode *DDPTimecode + DataOffset uint32 + DataLength uint16 + Flags DDPHeaderFlags + DataType DDPDataType + SequenceNumber uint8 + SourceOrDestinationID uint8 +} + +type DDPPacket struct { + Header DDPHeader + Data []byte +} + +func Decode(bytes []byte) (DDPPacket, error) { + if len(bytes) < 10 { + return DDPPacket{}, errors.New("DDP packet must be at least 10 bytes") + } + + flagsByte := bytes[0] + + flags := DDPHeaderFlags{ + Version: (flagsByte & 0xc0) >> 6, + Timecode: (flagsByte&0x10)>>4 == 1, + Storage: (flagsByte&0x08)>>3 == 1, + Reply: (flagsByte&0x04)>>2 == 1, + Query: (flagsByte&0x02)>>1 == 1, + Push: (flagsByte & 0x01) == 1, + } + + sequenceNumber := bytes[1] & 0x0f + + dataTypeByte := bytes[2] + + dataType := DDPDataType{ + Standard: dataTypeByte>>7 == 0, + Type: (dataTypeByte & 0x38) >> 3, + BitsPerPixel: dataTypeByte & 0x07, + } + + if dataType.BitsPerPixel > 1 { + if dataType.BitsPerPixel == 2 { + dataType.BitsPerPixel = 4 + } else if dataType.BitsPerPixel == 3 { + dataType.BitsPerPixel = 8 + } else if dataType.BitsPerPixel == 4 { + dataType.BitsPerPixel = 16 + } else if dataType.BitsPerPixel == 5 { + dataType.BitsPerPixel = 24 + } else if dataType.BitsPerPixel == 6 { + dataType.BitsPerPixel = 32 + } else { + return DDPPacket{}, errors.New("unknown DDP size") + } + } + + sourceOrDestinationID := bytes[3] + dataOffset := (uint32(bytes[4]) << 24) + (uint32(bytes[5]) << 16) + (uint32(bytes[6]) << 8) + uint32(bytes[7]) + dataLength := (uint16(bytes[8]) << 8) + uint16(bytes[9]) + + header := DDPHeader{ + Flags: flags, + SequenceNumber: sequenceNumber, + DataType: dataType, + SourceOrDestinationID: sourceOrDestinationID, + DataOffset: dataOffset, + DataLength: dataLength, + } + + dataStart := 10 + if flags.Timecode { + if len(bytes) < 14 { + return DDPPacket{}, errors.New("DDP packet with timecode must be at least 14 bytes") + } + dataStart = 14 + header.Timecode = &DDPTimecode{ + Seconds: uint16(bytes[10])<<8 + uint16(bytes[11]), + FractionalSeconds: uint16(bytes[12])<<8 + uint16(bytes[13]), + } + } + + return DDPPacket{ + Header: header, + Data: bytes[dataStart:], + }, nil +} + +func Encode(packet DDPPacket) ([]byte, error) { + flagByte := packet.Header.Flags.Version << 6 + + if packet.Header.Flags.Timecode { + flagByte += 16 + } + + if packet.Header.Flags.Storage { + flagByte += 8 + } + if packet.Header.Flags.Reply { + flagByte += 4 + } + if packet.Header.Flags.Query { + flagByte += 2 + } + if packet.Header.Flags.Push { + flagByte += 1 + } + + var dataTypeByte uint8 = 0 + + if !packet.Header.DataType.Standard { + dataTypeByte += 128 + } + + dataTypeByte += packet.Header.DataType.Type << 3 + + if packet.Header.DataType.BitsPerPixel == 1 { + dataTypeByte += 1 + } else if packet.Header.DataType.BitsPerPixel == 4 { + dataTypeByte += 2 + } else if packet.Header.DataType.BitsPerPixel == 8 { + dataTypeByte += 3 + } else if packet.Header.DataType.BitsPerPixel == 16 { + dataTypeByte += 4 + } else if packet.Header.DataType.BitsPerPixel == 24 { + dataTypeByte += 5 + } else if packet.Header.DataType.BitsPerPixel == 32 { + dataTypeByte += 6 + } else if packet.Header.DataType.BitsPerPixel == 0 { + dataTypeByte += 0 + } else { + return []byte{}, errors.New("DDP packet has unsupported bits per pixel value") + } + + bytes := []byte{} + + bytes = append(bytes, flagByte) + bytes = append(bytes, packet.Header.SequenceNumber) + bytes = append(bytes, dataTypeByte) + bytes = append(bytes, packet.Header.SourceOrDestinationID) + + bytes = binary.BigEndian.AppendUint32(bytes, packet.Header.DataOffset) + bytes = binary.BigEndian.AppendUint16(bytes, packet.Header.DataLength) + + if packet.Header.Flags.Timecode { + if packet.Header.Timecode != nil { + bytes = binary.BigEndian.AppendUint16(bytes, packet.Header.Timecode.Seconds) + bytes = binary.BigEndian.AppendUint16(bytes, packet.Header.Timecode.FractionalSeconds) + } else { + return []byte{}, errors.New("DDP packet had timecode flag set but no timecode value supplied") + } + } + + // const bytes = new Uint8Array([flagByte, sequenceNumberByte, dataTypeByte, packet.Header.sourceOrDestinationID]) + + bytes = append(bytes, packet.Data...) + return bytes, nil +} diff --git a/ddp_test.go b/ddp_test.go new file mode 100644 index 0000000..7e2e1e7 --- /dev/null +++ b/ddp_test.go @@ -0,0 +1,169 @@ +package ddp + +import ( + "fmt" + "reflect" + "testing" +) + +func TestPacketDecoding(t *testing.T) { + testCases := []struct { + description string + bytes []byte + expected DDPPacket + }{ + { + description: "simple DDP message", + bytes: []byte{0x40, 0x0f, 0x92, 0x01, 0x45, 0x67, 0x89, 0x10, 0x10, 0x11}, + expected: DDPPacket{ + Header: DDPHeader{ + Flags: DDPHeaderFlags{ + Version: 1, + Timecode: false, + Storage: false, + Reply: false, + Query: false, + Push: false, + }, + SequenceNumber: 15, + DataType: DDPDataType{ + Standard: false, + Type: 2, + BitsPerPixel: 4, + }, + SourceOrDestinationID: 1, + DataOffset: 1164413200, + DataLength: 4113, + }, + Data: []byte{}, + }, + }, + { + description: "DDP message with timecode", + bytes: []byte{0x50, 0x0f, 0x92, 0x01, 0x45, 0x67, 0x89, 0x10, 0x10, 0x11, 0x10, 0x11, 0x11, 0x10}, + expected: DDPPacket{ + Header: DDPHeader{ + Flags: DDPHeaderFlags{ + Version: 1, + Timecode: true, + Storage: false, + Reply: false, + Query: false, + Push: false, + }, + SequenceNumber: 15, + DataType: DDPDataType{ + Standard: false, + Type: 2, + BitsPerPixel: 4, + }, + SourceOrDestinationID: 1, + DataOffset: 1164413200, + DataLength: 4113, + Timecode: &DDPTimecode{ + Seconds: 4113, + FractionalSeconds: 4368, + }, + }, + Data: []byte{}, + }, + }, + } + + for _, testCase := range testCases { + + actual, err := Decode(testCase.bytes) + + if err != nil { + t.Errorf("Test '%s' failed to decode chunk properly", testCase.description) + fmt.Println(err) + } + + if !reflect.DeepEqual(actual, testCase.expected) { + t.Errorf("Test '%s' failed to decode chunk properly", testCase.description) + fmt.Printf("expected: %+v\n", testCase.expected) + fmt.Printf("actual: %+v\n", actual) + } + } +} + +func TestPacketEncoding(t *testing.T) { + testCases := []struct { + description string + expected []byte + packet DDPPacket + }{ + { + description: "simple DDP message", + expected: []byte{0x40, 0x0f, 0x92, 0x01, 0x45, 0x67, 0x89, 0x10, 0x10, 0x11}, + packet: DDPPacket{ + Header: DDPHeader{ + Flags: DDPHeaderFlags{ + Version: 1, + Timecode: false, + Storage: false, + Reply: false, + Query: false, + Push: false, + }, + SequenceNumber: 15, + DataType: DDPDataType{ + Standard: false, + Type: 2, + BitsPerPixel: 4, + }, + SourceOrDestinationID: 1, + DataOffset: 1164413200, + DataLength: 4113, + }, + Data: []byte{}, + }, + }, + { + description: "DDP message with timecode", + expected: []byte{0x50, 0x0f, 0x92, 0x01, 0x45, 0x67, 0x89, 0x10, 0x10, 0x11, 0x10, 0x11, 0x11, 0x10}, + packet: DDPPacket{ + Header: DDPHeader{ + Flags: DDPHeaderFlags{ + Version: 1, + Timecode: true, + Storage: false, + Reply: false, + Query: false, + Push: false, + }, + SequenceNumber: 15, + DataType: DDPDataType{ + Standard: false, + Type: 2, + BitsPerPixel: 4, + }, + SourceOrDestinationID: 1, + DataOffset: 1164413200, + DataLength: 4113, + Timecode: &DDPTimecode{ + Seconds: 4113, + FractionalSeconds: 4368, + }, + }, + Data: []byte{}, + }, + }, + } + + for _, testCase := range testCases { + + actual, err := Encode(testCase.packet) + + if err != nil { + t.Errorf("Test '%s' failed to decode chunk properly", testCase.description) + fmt.Println(err) + } + + if !reflect.DeepEqual(actual, testCase.expected) { + t.Errorf("Test '%s' failed to decode chunk properly", testCase.description) + fmt.Printf("expected: %+v\n", testCase.expected) + fmt.Printf("actual: %+v\n", actual) + } + } +} diff --git a/examples/wled.go b/examples/wled.go new file mode 100644 index 0000000..481d717 --- /dev/null +++ b/examples/wled.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "log/slog" + "net" + + "github.com/jwetzell/ddp-go" +) + +// receive data from WLED virtual output +func main() { + addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:4048") + if err != nil { + slog.Error("error making UDP address", "err", err) + return + } + + client, err := net.ListenUDP("udp", addr) + if err != nil { + slog.Error("error listening to UDP", "err", err) + return + } + defer client.Close() + + for { + buffer := make([]byte, 2048) + + length, _, err := client.ReadFromUDP(buffer) + if err != nil { + slog.Error("error reading from UDP", "err", err) + } else if length > 0 { + message, err := ddp.Decode(buffer[0:length]) + if err != nil { + slog.Error("error decoding", "err", err) + } + ledCount := uint32(message.Header.DataLength) / uint32(message.Header.DataType.BitsPerPixel/8) / 3 + + for i := 0; i < int(ledCount); i++ { + ledOffset := i * 3 + ledR := message.Data[ledOffset+0] + ledG := message.Data[ledOffset+1] + ledB := message.Data[ledOffset+2] + + fmt.Printf("led %d:\tr=%d\tg=%d\tb=%d\n", i, ledR, ledG, ledB) + } + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..287901e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/jwetzell/ddp-go + +go 1.23.4