initial commit

This commit is contained in:
2024-12-26 17:40:52 -06:00
commit 888ab65595
5 changed files with 409 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# ddp-go
Collection of [DDP](http://www.3waylabs.com/ddp/) things written in Go
- [DDP encoding/decoding library](./ddp.go)
+184
View File
@@ -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
}
+169
View File
@@ -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)
}
}
}
+49
View File
@@ -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)
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
module github.com/jwetzell/ddp-go
go 1.23.4