mirror of
https://github.com/jwetzell/ddp-go.git
synced 2026-08-14 19:43:40 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# ddp-go
|
||||
Collection of [DDP](http://www.3waylabs.com/ddp/) things written in Go
|
||||
|
||||
- [DDP encoding/decoding library](./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
|
||||
}
|
||||
+169
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user