You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by sr...@apache.org on 2024/08/19 19:16:19 UTC

(plc4x) 07/07: feat(plc4go/bacnet): add primitive tag and taglist

This is an automated email from the ASF dual-hosted git repository.

sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git

commit 8ecdf2bac4ae3c69e763190d74c8ba2c4688eb0a
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Mon Aug 19 21:14:26 2024 +0200

    feat(plc4go/bacnet): add primitive tag and taglist
---
 plc4go/internal/bacnetip/PDU.go                    | 159 ++++++-
 plc4go/internal/bacnetip/comp.go                   |  39 +-
 plc4go/internal/bacnetip/primitivedata.go          | 419 ++++++++++++++++-
 .../tests/test_primitive_data/test_tag_test.go     | 518 ++++++++++++++++++++-
 4 files changed, 1112 insertions(+), 23 deletions(-)

diff --git a/plc4go/internal/bacnetip/PDU.go b/plc4go/internal/bacnetip/PDU.go
index 301fa79137..2b3a933e3e 100644
--- a/plc4go/internal/bacnetip/PDU.go
+++ b/plc4go/internal/bacnetip/PDU.go
@@ -20,17 +20,22 @@
 package bacnetip
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
-	readWriteModel "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
-	"github.com/apache/plc4x/plc4go/spi"
-	"github.com/pkg/errors"
-	"github.com/rs/zerolog"
+	"io"
 	"net"
 	"net/netip"
 	"regexp"
 	"strconv"
 	"strings"
+
+	readWriteModel "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+	"github.com/apache/plc4x/plc4go/spi"
+	"github.com/apache/plc4x/plc4go/spi/utils"
+
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog"
 )
 
 var _shortMask = 0xFFFF
@@ -873,6 +878,126 @@ func (p *_PCI) String() string {
 	return fmt.Sprintf("_PCI{%s, expectingReply: %t, networkPriority: %s}", p.__PCI, p.expectingReply, p.networkPriority)
 }
 
+type PDUData interface {
+	GetPduData() []byte
+	Get() (byte, error)
+	GetShort() (int16, error)
+	GetLong() (int64, error)
+	GetData(dlen int) ([]byte, error)
+	Put(byte)
+	PutData(...byte)
+	PutShort(int16)
+	PutLong(int64)
+}
+
+type _PDUDataRequirements interface {
+	getPDUData() []byte
+}
+
+// _PDUData is basically a bridge to spi.Message
+type _PDUData struct {
+	_PDUDataRequirements
+
+	cachedData []byte
+	done       bool
+}
+
+var _ PDUData = (*_PDUData)(nil)
+
+func newPDUData(requirements _PDUDataRequirements) *_PDUData {
+	return &_PDUData{
+		_PDUDataRequirements: requirements,
+	}
+}
+
+func NewPDUData(args Args) PDUData {
+	return newPDUData(args[0].(_PDUDataRequirements))
+}
+
+func (d *_PDUData) GetPduData() []byte {
+	d.checkCache()
+	return d.cachedData
+}
+
+func (d *_PDUData) Get() (byte, error) {
+	d.checkCache()
+	if d.cachedData == nil {
+		return 0, io.EOF
+	}
+	octet := d.cachedData[0]
+	d.cachedData = d.cachedData[1:]
+	if d.cachedData == nil {
+		d.done = true
+	}
+	return octet, nil
+}
+
+func (d *_PDUData) GetData(dlen int) ([]byte, error) {
+	d.checkCache()
+	if len(d.cachedData) < dlen {
+		return nil, io.EOF
+	}
+	data := d.cachedData[:dlen]
+	d.cachedData = d.cachedData[dlen:]
+	if d.cachedData == nil {
+		d.done = true
+	}
+	return data, nil
+}
+
+func (d *_PDUData) GetShort() (int16, error) {
+	d.checkCache()
+	data, err := d.GetData(2)
+	if err != nil {
+		return 0, err
+	}
+	return int16(binary.BigEndian.Uint16(data)), nil
+}
+
+func (d *_PDUData) GetLong() (int64, error) {
+	d.checkCache()
+	data, err := d.GetData(4)
+	if err != nil {
+		return 0, err
+	}
+	return int64(binary.BigEndian.Uint64(data)), nil
+}
+
+func (d *_PDUData) Put(n byte) {
+	d.checkCache()
+	d.cachedData = append(d.cachedData, n)
+}
+
+func (d *_PDUData) PutData(n ...byte) {
+	d.checkCache()
+	d.cachedData = append(d.cachedData, n...)
+}
+
+func (d *_PDUData) PutShort(n int16) {
+	d.checkCache()
+	ba := make([]byte, 2)
+	binary.BigEndian.PutUint16(ba, uint16(n))
+	d.cachedData = append(d.cachedData, ba...)
+}
+
+func (d *_PDUData) PutLong(n int64) {
+	d.checkCache()
+	ba := make([]byte, 4)
+	binary.BigEndian.PutUint64(ba, uint64(n))
+	d.cachedData = append(d.cachedData, ba...)
+}
+
+func (d *_PDUData) checkCache() {
+	if !d.done && d.cachedData == nil {
+		d.cachedData = d.getPDUData()
+	}
+}
+
+func (d *_PDUData) deepCopy() *_PDUData {
+	copyPDUData := *d
+	return &copyPDUData
+}
+
 type PDU interface {
 	fmt.Stringer
 	GetMessage() spi.Message
@@ -887,43 +1012,49 @@ type PDU interface {
 
 type _PDU struct {
 	*_PCI
+	*_PDUData
 }
 
 func NewPDU(msg spi.Message, pduOptions ...PDUOption) PDU {
 	p := &_PDU{
-		newPCI(msg, nil, nil, false, readWriteModel.NPDUNetworkPriority_NORMAL_MESSAGE),
+		_PCI: newPCI(msg, nil, nil, false, readWriteModel.NPDUNetworkPriority_NORMAL_MESSAGE),
 	}
 	for _, option := range pduOptions {
 		option(p)
 	}
+	p._PDUData = newPDUData(p)
 	return p
 }
 
 func NewPDUFromPDU(pdu PDU, pduOptions ...PDUOption) PDU {
 	msg := pdu.(*_PDU).pduUserData
 	p := &_PDU{
-		newPCI(msg, pdu.GetPDUSource(), pdu.GetPDUDestination(), pdu.GetExpectingReply(), pdu.GetNetworkPriority()),
+		_PCI: newPCI(msg, pdu.GetPDUSource(), pdu.GetPDUDestination(), pdu.GetExpectingReply(), pdu.GetNetworkPriority()),
 	}
 	for _, option := range pduOptions {
 		option(p)
 	}
+	p._PDUData = newPDUData(p)
 	return p
 }
 
 func NewPDUFromPDUWithNewMessage(pdu PDU, msg spi.Message, pduOptions ...PDUOption) PDU {
 	p := &_PDU{
-		newPCI(msg, pdu.GetPDUSource(), pdu.GetPDUDestination(), pdu.GetExpectingReply(), pdu.GetNetworkPriority()),
+		_PCI: newPCI(msg, pdu.GetPDUSource(), pdu.GetPDUDestination(), pdu.GetExpectingReply(), pdu.GetNetworkPriority()),
 	}
 	for _, option := range pduOptions {
 		option(p)
 	}
+	p._PDUData = newPDUData(p)
 	return p
 }
 
 func NewPDUWithAllOptions(msg spi.Message, pduSource *Address, pduDestination *Address, expectingReply bool, networkPriority readWriteModel.NPDUNetworkPriority) *_PDU {
-	return &_PDU{
-		newPCI(msg, pduSource, pduDestination, expectingReply, networkPriority),
+	p := &_PDU{
+		_PCI: newPCI(msg, pduSource, pduDestination, expectingReply, networkPriority),
 	}
+	p._PDUData = newPDUData(p)
+	return p
 }
 
 type PDUOption func(pdu *_PDU)
@@ -952,6 +1083,14 @@ func WithPDUNetworkPriority(networkPriority readWriteModel.NPDUNetworkPriority)
 	}
 }
 
+func (p *_PDU) getPDUData() []byte {
+	writeBufferByteBased := utils.NewWriteBufferByteBased()
+	if err := p.GetMessage().SerializeWithWriteBuffer(context.Background(), writeBufferByteBased); err != nil {
+		panic(err) // TODO: graceful handle
+	}
+	return writeBufferByteBased.GetBytes()
+}
+
 func (p *_PDU) GetMessage() spi.Message {
 	return p.pduUserData
 }
@@ -981,7 +1120,7 @@ func (p *_PDU) GetNetworkPriority() readWriteModel.NPDUNetworkPriority {
 }
 
 func (p *_PDU) deepCopy() *_PDU {
-	return &_PDU{p._PCI.deepCopy()}
+	return &_PDU{_PCI: p._PCI.deepCopy(), _PDUData: p._PDUData.deepCopy()}
 }
 
 func (p *_PDU) DeepCopy() PDU {
diff --git a/plc4go/internal/bacnetip/comp.go b/plc4go/internal/bacnetip/comp.go
index 099b9a5197..611557a881 100644
--- a/plc4go/internal/bacnetip/comp.go
+++ b/plc4go/internal/bacnetip/comp.go
@@ -19,7 +19,13 @@
 
 package bacnetip
 
-import "fmt"
+import (
+	"context"
+	"fmt"
+
+	"github.com/apache/plc4x/plc4go/spi"
+	"github.com/apache/plc4x/plc4go/spi/utils"
+)
 
 type Arg any
 
@@ -99,3 +105,34 @@ const (
 	kwActorError = knownKey("actorError")
 	kwError      = knownKey("error")
 )
+
+type MessageBridge struct {
+	Bytes []byte
+}
+
+var _ spi.Message = (*MessageBridge)(nil)
+var _ _PDUDataRequirements = (*MessageBridge)(nil)
+
+func (m *MessageBridge) String() string {
+	return Btox(m.Bytes)
+}
+
+func (m *MessageBridge) Serialize() ([]byte, error) {
+	return m.Bytes, nil
+}
+
+func (m *MessageBridge) SerializeWithWriteBuffer(_ context.Context, writeBuffer utils.WriteBuffer) error {
+	return writeBuffer.WriteByteArray("Bytes", m.Bytes)
+}
+
+func (m *MessageBridge) GetLengthInBytes(_ context.Context) uint16 {
+	return uint16(len(m.Bytes))
+}
+
+func (m *MessageBridge) GetLengthInBits(ctx context.Context) uint16 {
+	return m.GetLengthInBytes(ctx) * 8
+}
+
+func (m *MessageBridge) getPDUData() []byte {
+	return m.Bytes
+}
diff --git a/plc4go/internal/bacnetip/primitivedata.go b/plc4go/internal/bacnetip/primitivedata.go
index 6d1f69af50..e446ae661f 100644
--- a/plc4go/internal/bacnetip/primitivedata.go
+++ b/plc4go/internal/bacnetip/primitivedata.go
@@ -26,6 +26,7 @@ import (
 	"fmt"
 	"math"
 	"regexp"
+	"slices"
 	"strconv"
 	"strings"
 	"time"
@@ -87,7 +88,9 @@ func NewTag(args Args) (*Tag, error) {
 		return t, nil
 	}
 	if len(args) == 1 {
-		t.decode(args[0])
+		if err := t.Decode(args[0].(PDUData)); err != nil {
+			return nil, errors.New("error decoding")
+		}
 	} else if len(args) >= 2 {
 		t.set(args)
 	} else {
@@ -96,7 +99,142 @@ func NewTag(args Args) (*Tag, error) {
 	return t, nil
 }
 
-func (t *Tag) decode(arg any) {
+func (t *Tag) Decode(pdu PDUData) error {
+	tag, err := pdu.Get()
+	if err != nil {
+		return errors.Wrap(err, "error decoding tag")
+	}
+
+	// extract the type
+	t.tagClass = model.TagClass(tag >> 3 & 0x01)
+
+	// extract the tag number
+	t.tagNumber = uint(tag >> 4)
+	if t.tagNumber == 0x0f {
+		get, err := pdu.Get()
+		if err != nil {
+			return errors.Wrap(err, "error decoding get")
+		}
+		t.tagNumber = uint(get)
+	}
+
+	// extract the length
+	t.tagLVT = int(tag & 0x07)
+	if t.tagLVT == 5 {
+		get, err := pdu.Get()
+		if err != nil {
+			return errors.Wrap(err, "error decoding get")
+		}
+		t.tagLVT = int(get)
+		if t.tagLVT == 254 {
+			get, err := pdu.GetShort()
+			if err != nil {
+				return errors.Wrap(err, "error decoding get")
+			}
+			t.tagLVT = int(get)
+		} else if t.tagLVT == 255 {
+			get, err := pdu.GetLong()
+			if err != nil {
+				return errors.Wrap(err, "error decoding get")
+			}
+			t.tagLVT = int(get)
+		}
+	} else if t.tagLVT == 6 {
+		t.tagClass = TagOpeningTagClass
+		t.tagLVT = 0
+	} else if t.tagLVT == 7 {
+		t.tagClass = TagClosingTagClass
+		t.tagLVT = 0
+	}
+
+	if t.tagClass == model.TagClass_APPLICATION_TAGS && t.tagNumber == uint(model.BACnetDataType_BOOLEAN) {
+		// tagLVT contains value
+		t.tagData = []byte{}
+	} else {
+		// tagLVT contains length
+		t.tagData, err = pdu.GetData(t.tagLVT)
+		if err != nil {
+			return errors.Wrap(err, "error decoding tag data")
+		}
+	}
+	return nil
+}
+
+func (t *Tag) Encode(pdu PDUData) {
+	var data byte
+	// check for special encoding
+	if t.tagClass == model.TagClass_CONTEXT_SPECIFIC_TAGS {
+		data = 0x08
+	} else if t.tagClass == TagOpeningTagClass {
+		data = 0x0E
+	} else if t.tagClass == TagClosingTagClass {
+		data = 0x0F
+	} else {
+		data = 0
+	}
+
+	// encode the tag number part
+	if t.tagNumber < 15 {
+		data += byte(t.tagNumber) << 4
+	} else {
+		data += 0x05
+	}
+
+	// encode the length/value/type part
+	if t.tagLVT < 5 {
+		data += byte(t.tagLVT)
+	} else {
+		data += 0xF0
+	}
+
+	// save this and the extended tag value
+	pdu.Put(data)
+	if t.tagNumber >= 15 {
+		pdu.Put(byte(t.tagNumber))
+	}
+
+	// really short lengths are already done
+	if t.tagLVT >= 5 {
+		if t.tagLVT <= 253 {
+			pdu.Put(byte(t.tagLVT))
+		} else if t.tagLVT <= 65535 {
+			pdu.Put(254)
+			pdu.PutShort(int16(t.tagLVT))
+		} else {
+			pdu.Put(255)
+			pdu.PutLong(int64(t.tagLVT))
+		}
+	}
+
+	// now put the data
+	pdu.PutData(t.tagData...)
+}
+
+func (t *Tag) AppToContext(context uint) (*ContextTag, error) {
+	if t.tagClass != model.TagClass_APPLICATION_TAGS {
+		return nil, errors.New("application tag required")
+	}
+	if t.tagNumber == uint(model.BACnetDataType_BOOLEAN) {
+		return NewContextTag(NewArgs(context, []byte{byte(t.tagLVT)}))
+	}
+	return NewContextTag(NewArgs(context, t.tagData))
+}
+
+func (t *Tag) ContextToApp(context uint) (any, error) {
+	if t.tagClass != model.TagClass_CONTEXT_SPECIFIC_TAGS {
+		return nil, errors.New("context tag required")
+	}
+	if t.tagNumber == uint(model.BACnetDataType_BOOLEAN) {
+		return NewTag(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_BOOLEAN, t.tagData[0], []byte{}))
+	}
+	return NewApplicationTag(NewArgs(context, t.tagData))
+}
+
+// TODO: pretty sure that is not tagListItem but we use that for now because lazy
+func (t *Tag) AppToObject() (TagListItem, error) {
+	if t.tagClass != model.TagClass_APPLICATION_TAGS {
+		return nil, errors.New("context tag required")
+	}
 	panic("implement me")
 }
 
@@ -129,6 +267,9 @@ func (t *Tag) set(args Args) {
 		return
 	}
 	t.tagData = args[3].([]byte)
+	if len(args) > 4 {
+		panic("oh no")
+	}
 }
 
 func (t *Tag) setAppData(tagNumber uint, tdata []byte) {
@@ -146,6 +287,10 @@ func (t *Tag) GetTagNumber() uint {
 	return t.tagNumber
 }
 
+func (t *Tag) GetTagLvt() int {
+	return t.tagLVT
+}
+
 func (t *Tag) GetTagData() []byte {
 	return t.tagData
 }
@@ -168,25 +313,279 @@ func (t *Tag) Equals(other any) bool {
 }
 
 type ApplicationTag struct {
-	model.BACnetApplicationTag
-	// TODO: implement me
+	*Tag
+}
+
+func NewApplicationTag(args Args) (*ApplicationTag, error) {
+	a := &ApplicationTag{}
+	switch len(args) {
+	case 1:
+		if _, ok := args[0].(PDUData); !ok {
+			return nil, errors.New("invalid argument")
+		}
+		var err error
+		a.Tag, err = NewTag(args)
+		if err != nil {
+			return nil, errors.New("error creating tag")
+		}
+		if a.tagClass != model.TagClass_APPLICATION_TAGS {
+			return nil, errors.New("error creating tag: invalid tag class")
+		}
+		return a, nil
+	case 2:
+		var tnum any
+		tnum, ok := args[0].(uint)
+		if !ok {
+			tnum, ok = args[0].(int)
+			if !ok {
+				return nil, errors.New("error creating tag: invalid tag number")
+			}
+		}
+		tdata := args[1].([]byte)
+		var err error
+		a.Tag, err = NewTag(NewArgs(model.TagClass_APPLICATION_TAGS, tnum, len(tdata), tdata))
+		if err != nil {
+			return nil, errors.New("error creating tag")
+		}
+		return a, nil
+	default:
+		return nil, errors.New("requires type and data or pdu data")
+	}
 }
 
 type ContextTag struct {
-	model.BACnetContextTag
-	// TODO: implement me
+	*Tag
+}
+
+func NewContextTag(args Args) (*ContextTag, error) {
+	c := &ContextTag{}
+	switch len(args) {
+	case 1:
+		if _, ok := args[0].(PDUData); !ok {
+			return nil, errors.New("invalid argument")
+		}
+		var err error
+		c.Tag, err = NewTag(args)
+		if err != nil {
+			return nil, errors.New("error creating tag")
+		}
+		if c.tagClass != model.TagClass_CONTEXT_SPECIFIC_TAGS {
+			return nil, errors.New("error creating tag: invalid tag class")
+		}
+		return c, nil
+	case 2:
+		var tnum any
+		tnum, ok := args[0].(uint)
+		if !ok {
+			tnum, ok = args[0].(int)
+			if !ok {
+				return nil, errors.New("error creating tag: invalid tag number")
+			}
+		}
+		tdata := args[1].([]byte)
+		var err error
+		c.Tag, err = NewTag(NewArgs(model.TagClass_CONTEXT_SPECIFIC_TAGS, tnum, len(tdata), tdata))
+		if err != nil {
+			return nil, errors.New("error creating tag")
+		}
+		return c, nil
+	default:
+		return nil, errors.New("requires type and data or pdu data")
+	}
 }
 
 type OpeningTag struct {
-	// TODO: implement me
+	*Tag
+}
+
+func NewOpeningTag(context Arg) (*OpeningTag, error) {
+	o := &OpeningTag{}
+	switch context.(type) {
+	case int, uint:
+		var err error
+		o.Tag, err = NewTag(NewArgs(TagOpeningTagClass, context))
+		if err != nil {
+			return nil, errors.Wrap(err, "error creating tag")
+		}
+		return o, nil
+	default:
+		return nil, errors.New("invalid argument")
+	}
 }
 
 type ClosingTag struct {
-	// TODO: implement me
+	*Tag
+}
+
+func NewClosingTag(context Arg) (*ClosingTag, error) {
+	o := &ClosingTag{}
+	switch context.(type) {
+	case int, uint:
+		var err error
+		o.Tag, err = NewTag(NewArgs(TagClosingTagClass, context))
+		if err != nil {
+			return nil, errors.Wrap(err, "error creating tag")
+		}
+		return o, nil
+	default:
+		return nil, errors.New("invalid argument")
+	}
+}
+
+// TODO: if Tag would be an interface we could get rid of this
+type TagListItem interface {
+	GetTagClass() model.TagClass
+	GetTagNumber() uint
+	GetTagLvt() int
+	GetTagData() []byte
+	Encode(pdu PDUData)
+	Decode(pdu PDUData) error
 }
 
 type TagList struct {
-	// TODO: implement me
+	tagList []TagListItem
+}
+
+func NewTagList(arg Arg) *TagList {
+	t := &TagList{}
+	switch arg := arg.(type) {
+	case []any:
+		args := arg
+		for _, a := range args {
+			t.tagList = append(t.tagList, a.(TagListItem))
+		}
+	case []TagListItem:
+		args := arg
+		for _, a := range args {
+			t.tagList = append(t.tagList, a.(TagListItem))
+		}
+	case Args:
+		args := arg
+		for _, a := range args {
+			t.tagList = append(t.tagList, a.(TagListItem))
+		}
+	}
+	return t
+}
+
+func (b *TagList) Append(tag *Tag) {
+	b.tagList = append(b.tagList, tag)
+}
+
+func (b *TagList) Extend(tags ...*Tag) {
+	for _, tag := range tags {
+		b.tagList = append(b.tagList, tag)
+	}
+}
+
+func (b *TagList) Peek() TagListItem {
+	if len(b.tagList) < 1 {
+		return nil
+	}
+	return b.tagList[0]
+}
+
+func (b *TagList) Push(tag TagListItem) {
+	b.tagList = append([]TagListItem{tag}, b.tagList...)
+}
+
+func (b *TagList) Pop() TagListItem {
+	if len(b.tagList) < 1 {
+		return nil
+	}
+	item := b.tagList[0]
+	b.tagList = b.tagList[1:]
+	return item
+}
+
+// GetContext Return a tag or a list of tags context encoded.
+func (b *TagList) GetContext(context uint) (any, error) {
+	// forward pass
+	i := 0
+	for i < len(b.tagList) {
+		tag := b.tagList[i]
+
+		switch tag.GetTagClass() {
+		case model.TagClass_APPLICATION_TAGS: // skip application stuff
+		case model.TagClass_CONTEXT_SPECIFIC_TAGS: // check for context encoded atomic value
+			if tag.GetTagNumber() == context {
+				return tag, nil
+			}
+		case TagOpeningTagClass:
+			keeper := tag.GetTagNumber() == context
+			var rslt []TagListItem
+			i += 1
+			lvl := 0
+		innerSearch:
+			for i < len(b.tagList) {
+				tag := b.tagList[i]
+				switch tag.GetTagClass() {
+				case TagOpeningTagClass:
+					lvl += 1
+				case TagClosingTagClass:
+					lvl -= 1
+					if lvl < 0 {
+						break innerSearch
+					}
+				}
+
+				rslt = append(rslt, tag)
+				i += 1
+			}
+
+			// make sure everything balances
+			if lvl >= 1 {
+				return nil, errors.New("mismatched open/close tag")
+			}
+
+			// get everything we need
+			if keeper {
+				return NewTagList(rslt), nil
+			}
+		}
+		i += 1
+	}
+	return nil, nil
+}
+
+func (b *TagList) Encode(data PDUData) {
+	for _, tag := range b.tagList {
+		tag.Encode(data)
+	}
+}
+
+func (b *TagList) Decode(data PDUData) error {
+	for len(data.GetPduData()) != 0 {
+		var tag TagListItem
+		tag, err := NewTag(NewArgs(data))
+		if err != nil {
+			return errors.Wrap(err, "error creating tag")
+		}
+		switch tag.GetTagClass() {
+		case model.TagClass_APPLICATION_TAGS:
+		case model.TagClass_CONTEXT_SPECIFIC_TAGS:
+			tag, err = NewContextTag(NewArgs(tag.GetTagNumber(), tag.GetTagData()))
+			if err != nil {
+				panic(err)
+			}
+		case model.TagClass(TagOpeningTagClass):
+			tag, err = NewOpeningTag(tag.GetTagNumber())
+			if err != nil {
+				panic(err)
+			}
+		case model.TagClass(TagClosingTagClass):
+			tag, err = NewClosingTag(tag.GetTagNumber())
+			if err != nil {
+				panic(err)
+			}
+		}
+		b.tagList = append(b.tagList, tag)
+	}
+	return nil
+}
+
+func (b *TagList) GetTagList() []TagListItem {
+	return b.tagList
 }
 
 type ComparableAndOrdered interface {
@@ -920,8 +1319,6 @@ func (d *Double) String() string {
 }
 
 type OctetString struct {
-	//*Atomic[...] no support for []byte
-
 	value []byte
 }
 
diff --git a/plc4go/internal/bacnetip/tests/test_primitive_data/test_tag_test.go b/plc4go/internal/bacnetip/tests/test_primitive_data/test_tag_test.go
index ed516b5011..24845bfaa1 100644
--- a/plc4go/internal/bacnetip/tests/test_primitive_data/test_tag_test.go
+++ b/plc4go/internal/bacnetip/tests/test_primitive_data/test_tag_test.go
@@ -19,4 +19,520 @@
 
 package test_primitive_data
 
-// TODO: implement me
+import (
+	"testing"
+
+	"github.com/apache/plc4x/plc4go/internal/bacnetip"
+	"github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func ApplicationTag(args ...any) *bacnetip.ApplicationTag {
+	tag, err := bacnetip.NewApplicationTag(args)
+	if err != nil {
+		panic(err)
+	}
+	return tag
+}
+
+func ContextTag(args ...any) *bacnetip.ContextTag {
+	tag, err := bacnetip.NewContextTag(args)
+	if err != nil {
+		panic(err)
+	}
+	return tag
+}
+
+func OpeningTag(context any) *bacnetip.OpeningTag {
+	openingTag, err := bacnetip.NewOpeningTag(context)
+	if err != nil {
+		panic(err)
+	}
+	return openingTag
+}
+
+func ClosingTag(context any) *bacnetip.ClosingTag {
+	closingTag, err := bacnetip.NewClosingTag(context)
+	if err != nil {
+		panic(err)
+	}
+	return closingTag
+}
+
+func TagList(tags ...bacnetip.TagListItem) *bacnetip.TagList {
+	return bacnetip.NewTagList(tags)
+}
+
+func PDUData(args ...any) bacnetip.PDUData {
+	if args == nil {
+		return bacnetip.NewPDUData(bacnetip.NewArgs(&bacnetip.MessageBridge{}))
+	} else {
+		return bacnetip.NewPDUData(bacnetip.NewArgs(&bacnetip.MessageBridge{Bytes: args[0].([]byte)}))
+	}
+}
+
+func TagTuple(tag *bacnetip.Tag) (tagClass model.TagClass, tagNumber uint, tagLVT int, tagData []byte) {
+	return tag.GetTagClass(), tag.GetTagNumber(), tag.GetTagLvt(), tag.GetTagData()
+}
+
+// Build PDU from the string, decode the tag, convert to an object.
+func objDecode(blob []byte) any {
+	data := PDUData(blob)
+	tag := Tag(data)
+	obj, err := tag.AppToObject()
+	if err != nil {
+		panic(err) // TODO
+	}
+	return obj
+}
+
+// Encode the object into a tag, encode it in a PDU, return the data.
+func objEncode(obj any) []byte {
+	tag := Tag()
+	obj.(interface{ Encode(any) }).Encode(tag) //TODO: what should we use
+	data := PDUData()
+	tag.Encode(data)
+	return data.GetPduData()
+}
+
+// Convert the value (a primitive object) to a hex encoded string,
+//
+//	convert the hex encoded string to and object, and compare the results to
+//	each other.
+func objEndec(t *testing.T, obj any, x string) {
+	// convert the hex strings to a blobs
+	blob := xtob(x)
+
+	// decode the blob into an object
+	obj2 := objDecode(blob)
+
+	// encode the tag into a blob
+	blob2 := objEncode(obj)
+
+	// compare the results
+	assert.Equal(t, obj, obj2)
+	assert.Equal(t, blob, blob2)
+}
+
+// Build PDU from the string, decode the tag, convert to an object.
+func contextDecode(blob []byte) *bacnetip.ContextTag {
+	data := PDUData(blob)
+	tag := ContextTag(data)
+	return tag
+}
+
+// Encode the object into a tag, encode it in a PDU, return the data.
+func contextEncode(tag *bacnetip.ContextTag) []byte {
+	data := PDUData()
+	tag.Encode(data)
+	return data.GetPduData()
+}
+
+// Convert the value (a primitive object) to a hex encoded string,
+// convert the hex encoded string to and object, and compare the results to
+// each other.
+func contextEndec(t *testing.T, tnum int, x string, y string) {
+	// convert the hex strings to a blobs
+	tdata := xtob(x)
+	blob1 := xtob(y)
+
+	// make a context tag
+	tag1 := ContextTag(tnum, tdata)
+
+	// decode the blob into a tag
+	tag2 := contextDecode(blob1)
+
+	// encode the tag into a blob
+	blob2 := contextEncode(tag1)
+
+	// compare the results
+	assert.Equal(t, tag1, tag2)
+	assert.Equal(t, blob1, blob2)
+}
+
+// Build PDU from the string, decode the tag, convert to an object.
+func openingDecode(blob []byte) *bacnetip.OpeningTag {
+	data := PDUData(blob)
+	tag := OpeningTag(data)
+	return tag
+}
+
+// Encode the object into a tag, encode it in a PDU, return the data.
+func openingEncode(tag *bacnetip.OpeningTag) []byte {
+	data := PDUData()
+	tag.Encode(data)
+	return data.GetPduData()
+}
+
+// Convert the value (a primitive object) to a hex encoded string,
+// convert the hex encoded string to and object, and compare the results to
+// each other.
+func openingEndec(t *testing.T, tnum int, x string) {
+
+	// convert the hex string to a blob
+	blob1 := xtob(x)
+
+	// make a context tag
+	tag1 := OpeningTag(tnum)
+
+	// decode the blob into a tag
+	tag2 := openingDecode(blob1)
+
+	// encode the tag into a blob
+	blob2 := openingEncode(tag1)
+
+	// compare the results
+	assert.Equal(t, tag1, tag2)
+	assert.Equal(t, blob1, blob2)
+}
+
+// Build PDU from the string, decode the tag, convert to an object.
+func closingDecode(blob []byte) *bacnetip.ClosingTag {
+	data := PDUData(blob)
+	tag := ClosingTag(data)
+	return tag
+}
+
+// Encode the object into a tag, encode it in a PDU, return the data.
+func closingEncode(tag *bacnetip.ClosingTag) []byte {
+	data := PDUData()
+	tag.Encode(data)
+	return data.GetPduData()
+}
+
+// Convert the value (a primitive object) to a hex encoded string,
+// convert the hex encoded string to and object, and compare the results to
+// each other.
+func closingEndec(t *testing.T, tnum int, x string) {
+	// convert the hex string to a blob
+	blob1 := xtob(x)
+
+	// make a context tag
+	tag1 := ClosingTag(tnum)
+
+	// decode the blob into a tag
+	tag2 := closingDecode(blob1)
+
+	// encode the tag into a blob
+	blob2 := closingEncode(tag1)
+
+	// compare the result
+	assert.Equal(t, tag1, tag2)
+	assert.Equal(t, blob1, blob2)
+}
+
+func TestTag(t *testing.T) {
+	tag := Tag()
+	assert.Equal(t, model.TagClass(0), tag.GetTagClass())
+	assert.Equal(t, uint(0), tag.GetTagNumber())
+
+	// must have a valid encoded tag to extract the from the data
+	data := PDUData(xtob(""))
+	assert.Panics(t, func() {
+		Tag(data)
+	})
+
+	// must have two values, class and number
+	assert.Panics(t, func() {
+		Tag(0)
+	})
+
+	tag = Tag(0, 1)
+	assert.Equal(t, model.TagClass(0), tag.GetTagClass())
+	assert.Equal(t, uint(1), tag.GetTagNumber())
+
+	tag = Tag(0, 1, 2)
+	assert.Equal(t, model.TagClass(0), tag.GetTagClass())
+	assert.Equal(t, uint(1), tag.GetTagNumber())
+	assert.Equal(t, 2, tag.GetTagLvt())
+
+	// tag data must be bytes of bytearray
+	assert.Panics(t, func() {
+		Tag(0, 1, 2, 3)
+	})
+}
+
+func TestApplicationTag(t *testing.T) {
+	tag := ApplicationTag(0, xtob(""))
+	_ = tag
+
+	assert.Panics(t, func() {
+		ApplicationTag(0)
+	})
+}
+
+func TestGenericApplicationToContext(t *testing.T) {
+	t.Skip("Not yet ready") // TODO: implement me
+	// create and application
+	tag := ApplicationTag(0, xtob("01"))
+
+	// convert it to context tagged, context 0
+	ctag, err := tag.AppToContext(0)
+	require.NoError(t, err)
+
+	// create a context tag with the same shape
+	ttag := ContextTag(0, xtob("00"))
+
+	// check to see if they are the same
+	assert.Equal(t, ctag, ttag)
+
+	// convert the context tag back to an application tag
+	dtag, err := ctag.ContextToApp(uint(model.BACnetDataType_BOOLEAN))
+	require.NoError(t, err)
+
+	// check to see it round-tripped
+	assert.Equal(t, dtag, tag)
+}
+
+func TestBooleanApplicationToContext(t *testing.T) {
+	t.Skip("Not yet ready") // TODO: implement me
+	// create and application
+	tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_BOOLEAN, 0)
+
+	// convert it to context tagged, context 0
+	ctag, err := tag.AppToContext(0)
+	require.NoError(t, err)
+
+	// create a context tag with the same shape
+	ttag := ContextTag(0, xtob("00"))
+
+	// check to see if they are the same
+	assert.Equal(t, ctag, ttag)
+
+	// convert the context tag back to an application tag
+	dtag, err := ctag.ContextToApp(uint(model.BACnetDataType_BOOLEAN))
+	require.NoError(t, err)
+
+	// check to see it round-tripped
+	assert.Equal(t, dtag, tag)
+}
+
+func TestBooleanApplicationToObject(t *testing.T) {
+	t.Skip("Not yet ready") // TODO: implement me
+	// null
+	objEndec(t, Null(), "00")
+
+	// boolean
+	objEndec(t, Boolean(true), "11")
+	objEndec(t, Boolean(true), "10")
+
+	// unsigned
+	objEndec(t, Unsigned(0), "2100")
+	objEndec(t, Unsigned(1), "2101")
+	objEndec(t, Unsigned(127), "217F")
+	objEndec(t, Unsigned(128), "2180")
+
+	// integer
+	objEndec(t, Integer(0), "3100")
+	objEndec(t, Integer(1), "3101")
+	objEndec(t, Integer(-1), "31FF")
+	objEndec(t, Integer(128), "320080")
+	objEndec(t, Integer(-128), "3180")
+
+	// real
+	objEndec(t, Real(0), "4400000000")
+	objEndec(t, Real(1), "443F800000")
+	objEndec(t, Real(-1), "44BF800000")
+	objEndec(t, Real(73.5), "4442930000")
+
+	// double
+	objEndec(t, Double(0), "55080000000000000000")
+	objEndec(t, Double(1), "55083FF0000000000000")
+	objEndec(t, Double(-1), "5508BFF0000000000000")
+	objEndec(t, Double(73.5), "55084052600000000000")
+
+	// octet string
+	objEndec(t, OctetString(xtob("")), "60")
+	objEndec(t, OctetString(xtob("01")), "6101")
+	objEndec(t, OctetString(xtob("0102")), "620102")
+	objEndec(t, OctetString(xtob("010203040506")), "6506010203040506")
+
+	// character string
+	objEndec(t, CharacterString(""), "7100")
+	objEndec(t, CharacterString("a"), "720061")
+	objEndec(t, CharacterString("abcde"), "7506006162636465")
+
+	// bit string
+	objEndec(t, BitString([]bool{}), "8100")
+	objEndec(t, BitString([]bool{false}), "820700")
+	objEndec(t, BitString([]bool{true}), "820780")
+	objEndec(t, BitString([]bool{true, true, true, true, true}), "8203F8")
+	objEndec(t, BitString([]bool{true, true, true, true, true, true, true, true, true, true}), "8306FFC0")
+
+	// enumerated
+	objEndec(t, Enumerated(0), "9100")
+	objEndec(t, Enumerated(1), "9101")
+	objEndec(t, Enumerated(127), "917F")
+	objEndec(t, Enumerated(128), "9180")
+
+	// date
+	objEndec(t, Date(bacnetip.DateTuple{1, 2, 3, 4}), "A401020304")
+	objEndec(t, Date(bacnetip.DateTuple{255, 2, 3, 4}), "A4FF020304")
+	objEndec(t, Date(bacnetip.DateTuple{1, 255, 3, 4}), "A401FF0304")
+	objEndec(t, Date(bacnetip.DateTuple{1, 2, 255, 4}), "A40102FF04")
+	objEndec(t, Date(bacnetip.DateTuple{1, 2, 3, 255}), "A4010203FF")
+
+	// time
+	objEndec(t, Time(bacnetip.TimeTuple{1, 2, 3, 4}), "B401020304")
+	objEndec(t, Time(bacnetip.TimeTuple{255, 2, 3, 4}), "B4FF020304")
+	objEndec(t, Time(bacnetip.TimeTuple{1, 255, 3, 4}), "B401FF0304")
+	objEndec(t, Time(bacnetip.TimeTuple{1, 2, 255, 4}), "B40102FF04")
+	objEndec(t, Time(bacnetip.TimeTuple{1, 2, 3, 255}), "B4010203FF")
+
+	// object identifier
+	objEndec(t, ObjectIdentifier(0, 0), "C400000000")
+	objEndec(t, ObjectIdentifier(1, 0), "C400400000")
+	objEndec(t, ObjectIdentifier(0, 2), "C400000002")
+	objEndec(t, ObjectIdentifier(3, 4), "C400C00004")
+}
+
+func TestContextTag(t *testing.T) {
+	t.Skip("Not yet ready") // TODO: implement me
+	ContextTag(0, xtob(""))
+
+	contextEndec(t, 0, "", "08")
+	contextEndec(t, 1, "01", "1901")
+	contextEndec(t, 2, "0102", "2A0102")
+	contextEndec(t, 3, "010203", "3B010203")
+	contextEndec(t, 14, "010203", "EB010203")
+	contextEndec(t, 15, "010203", "FB0F010203")
+}
+
+func TestOpeningTag(t *testing.T) {
+	t.Skip("Not yet ready") // TODO: implement me
+	OpeningTag(0)
+
+	openingEndec(t, 0, "0E")
+	openingEndec(t, 1, "1E")
+	openingEndec(t, 2, "2E")
+	openingEndec(t, 3, "3E")
+	openingEndec(t, 14, "EE")
+	openingEndec(t, 15, "FE0F")
+	openingEndec(t, 254, "FEFE")
+}
+
+func TestClosingTag(t *testing.T) {
+	t.Skip("Not yet ready") // TODO: implement me
+	ClosingTag(0)
+
+	closingEndec(t, 0, "0f")
+	closingEndec(t, 1, "1F")
+	closingEndec(t, 2, "2F")
+	closingEndec(t, 3, "3F")
+	closingEndec(t, 14, "EF")
+	closingEndec(t, 15, "FF0F")
+	closingEndec(t, 254, "FFFE")
+}
+
+func TestTagList(t *testing.T) {
+	_ = TagList()
+}
+
+func TestPeek(t *testing.T) {
+	tag0 := IntegerTag("00")
+	taglist := TagList(tag0)
+
+	// peek at the first tag
+	assert.Equal(t, tag0, taglist.Peek())
+
+	// pop of the front
+	tag1 := taglist.Pop()
+	var emptyList = make([]bacnetip.TagListItem, 0)
+	assert.Equal(t, emptyList, taglist.GetTagList())
+
+	// push if back to the front
+	taglist.Push(tag1)
+	assert.Equal(t, []bacnetip.TagListItem{tag1}, taglist.GetTagList())
+}
+
+func TestGetContext(t *testing.T) {
+	tagListData := []bacnetip.TagListItem{
+		ContextTag(0, xtob("00")),
+		ContextTag(1, xtob("01")),
+		OpeningTag(2),
+		IntegerTag("03"),
+		OpeningTag(0),
+		IntegerTag("04"),
+		ClosingTag(0),
+		ClosingTag(2),
+	}
+	taglist := TagList(tagListData...)
+
+	// known to be a simple context encoded element
+	context0, err := taglist.GetContext(0)
+	require.NoError(t, err)
+	assert.Equal(t, tagListData[0], context0)
+
+	// known to be a simple context encoded list of element(s)
+	context2, err := taglist.GetContext(2)
+	require.NoError(t, err)
+	assert.Equal(t, tagListData[3:7], context2.(*bacnetip.TagList).GetTagList())
+
+	// known missing context
+	context3, err := taglist.GetContext(3)
+	require.NoError(t, err)
+	assert.Equal(t, nil, context3)
+}
+
+func TestEndec0(t *testing.T) { // Test bracketed application tagged integer encoding and decoding.
+	tagList := TagList()
+
+	data := PDUData()
+	tagList.Encode(data)
+	var nothing []byte
+	assert.Equal(t, nothing, data.GetPduData())
+
+	tagList = TagList()
+	err := tagList.Decode(data)
+	assert.NoError(t, err)
+	var noItems []bacnetip.TagListItem
+	assert.Equal(t, noItems, tagList.GetTagList())
+}
+
+func TestEndec1(t *testing.T) { // Test bracketed application tagged integer encoding and decoding.
+	tag0 := IntegerTag("00")
+	tag1 := IntegerTag("01")
+	tagList := TagList(tag0, tag1)
+
+	data := PDUData()
+	tagList.Encode(data)
+	assert.Equal(t, xtob("31003101"), data.GetPduData())
+
+	tagList = TagList()
+	err := tagList.Decode(data)
+	assert.NoError(t, err)
+	assert.Equal(t, []bacnetip.TagListItem{tag0, tag1}, tagList.GetTagList())
+}
+
+func TestEndec2(t *testing.T) { // Test bracketed application tagged integer encoding and decoding.
+	tag0 := ContextTag(0, xtob("00"))
+	tag1 := ContextTag(1, xtob("01"))
+	tagList := TagList(tag0, tag1)
+
+	data := PDUData()
+	tagList.Encode(data)
+	assert.Equal(t, xtob("09001901"), data.GetPduData())
+
+	tagList = TagList()
+	err := tagList.Decode(data)
+	assert.NoError(t, err)
+	assert.Equal(t, []bacnetip.TagListItem{tag0, tag1}, tagList.GetTagList())
+}
+
+func TestEndec3(t *testing.T) { // Test bracketed application tagged integer encoding and decoding.
+	tag0 := OpeningTag(0)
+	tag1 := IntegerTag("0102")
+	tag2 := ClosingTag(0)
+	tagList := TagList(tag0, tag1, tag2)
+
+	data := PDUData()
+	tagList.Encode(data)
+	assert.Equal(t, xtob("0E3201020F"), data.GetPduData())
+
+	tagList = TagList()
+	err := tagList.Decode(data)
+	assert.NoError(t, err)
+	assert.Equal(t, []bacnetip.TagListItem{tag0, tag1, tag2}, tagList.GetTagList())
+}