feat(plc4go/bacnet): add primitive character string and date
diff --git a/plc4go/internal/bacnetip/debugging.go b/plc4go/internal/bacnetip/debugging.go
index 5c11427..0136cff 100644
--- a/plc4go/internal/bacnetip/debugging.go
+++ b/plc4go/internal/bacnetip/debugging.go
@@ -24,6 +24,10 @@
"regexp"
)
+func Btox(data []byte) string {
+ return hex.EncodeToString(data)
+}
+
func Xtob(hexString string) ([]byte, error) {
compile, err := regexp.Compile("[^0-9a-fA-F]")
if err != nil {
diff --git a/plc4go/internal/bacnetip/primitivedata.go b/plc4go/internal/bacnetip/primitivedata.go
index 3d76709..398ac56 100644
--- a/plc4go/internal/bacnetip/primitivedata.go
+++ b/plc4go/internal/bacnetip/primitivedata.go
@@ -22,8 +22,12 @@
import (
"bytes"
"cmp"
+ "encoding/binary"
"fmt"
+ "regexp"
+ "strconv"
"strings"
+ "time"
"github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
@@ -256,8 +260,76 @@
// TODO: implement me
}
+// TODO: finish
type Null struct {
- // TODO: implement me
+ *Atomic[int]
+}
+
+func NewNull(arg Arg) (*Null, error) {
+ b := &Null{}
+ b.Atomic = NewAtomic[int](b)
+
+ if arg == nil {
+ return b, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := b.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return b, nil
+ case bool:
+ if arg {
+ b.value = 1
+ }
+ case *Null:
+ b.value = arg.value
+ case string:
+ switch arg {
+ case "True", "true":
+ b.value = 1
+ case "False", "false":
+ default:
+ return nil, errors.Errorf("invalid string: %s", arg)
+ }
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return b, nil
+}
+
+func (b *Null) Encode(tag *Tag) {
+ tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_NULL, b.value, []byte{}))
+}
+
+func (b *Null) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_NULL) {
+ return errors.New("Null application tag required")
+ }
+ if tag.tagLVT > 1 {
+ return errors.New("invalid tag value")
+ }
+
+ // get the data
+ if tag.tagLVT == 1 {
+ b.value = 1
+ }
+ return nil
+}
+
+func (b *Null) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (b *Null) String() string {
+ value := "False"
+ if b.value == 1 {
+ value = "True"
+ }
+ return fmt.Sprintf("Null(%s)", value)
}
type Boolean struct {
@@ -267,7 +339,6 @@
func NewBoolean(arg Arg) (*Boolean, error) {
b := &Boolean{}
b.Atomic = NewAtomic[int](b)
- b.value = 0 // atomic doesn't like bool
if arg == nil {
return b, nil
@@ -341,6 +412,595 @@
return fmt.Sprintf("Boolean(%s)", value)
}
+// TODO: finish
+type Unsigned struct {
+ *Atomic[uint]
+ *CommonMath
+}
+
+func NewUnsigned(arg Arg) (*Unsigned, error) {
+ b := &Unsigned{}
+ b.Atomic = NewAtomic[uint](b)
+
+ if arg == nil {
+ return b, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := b.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return b, nil
+ case bool:
+ if arg {
+ b.value = 1
+ }
+ case *Unsigned:
+ b.value = arg.value
+ case string:
+ switch arg {
+ case "True", "true":
+ b.value = 1
+ case "False", "false":
+ default:
+ return nil, errors.Errorf("invalid string: %s", arg)
+ }
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return b, nil
+}
+
+func (b *Unsigned) Encode(tag *Tag) {
+ tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_UNSIGNED_INTEGER, b.value, []byte{}))
+}
+
+func (b *Unsigned) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_UNSIGNED_INTEGER) {
+ return errors.New("Unsigned application tag required")
+ }
+ if tag.tagLVT > 1 {
+ return errors.New("invalid tag value")
+ }
+
+ // get the data
+ if tag.tagLVT == 1 {
+ b.value = 1
+ }
+ return nil
+}
+
+func (b *Unsigned) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (b *Unsigned) String() string {
+ value := "False"
+ if b.value == 1 {
+ value = "True"
+ }
+ return fmt.Sprintf("Unsigned(%s)", value)
+}
+
+// TODO: finish
+type Unsigned8 struct {
+ *Atomic[uint8]
+}
+
+func NewUnsigned8(arg Arg) (*Unsigned8, error) {
+ b := &Unsigned8{}
+ b.Atomic = NewAtomic[uint8](b)
+
+ if arg == nil {
+ return b, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := b.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return b, nil
+ case bool:
+ if arg {
+ b.value = 1
+ }
+ case *Unsigned8:
+ b.value = arg.value
+ case string:
+ switch arg {
+ case "True", "true":
+ b.value = 1
+ case "False", "false":
+ default:
+ return nil, errors.Errorf("invalid string: %s", arg)
+ }
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return b, nil
+}
+
+func (b *Unsigned8) Encode(tag *Tag) {
+ tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_UNSIGNED_INTEGER, b.value, []byte{}))
+}
+
+func (b *Unsigned8) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_UNSIGNED_INTEGER) {
+ return errors.New("Unsigned8 application tag required")
+ }
+ if tag.tagLVT > 1 {
+ return errors.New("invalid tag value")
+ }
+
+ // get the data
+ if tag.tagLVT == 1 {
+ b.value = 1
+ }
+ return nil
+}
+
+func (b *Unsigned8) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (b *Unsigned8) String() string {
+ value := "False"
+ if b.value == 1 {
+ value = "True"
+ }
+ return fmt.Sprintf("Unsigned8(%s)", value)
+}
+
+// TODO: finish
+type Unsigned16 struct {
+ *Atomic[uint16]
+}
+
+func NewUnsigned16(arg Arg) (*Unsigned16, error) {
+ b := &Unsigned16{}
+ b.Atomic = NewAtomic[uint16](b)
+
+ if arg == nil {
+ return b, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := b.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return b, nil
+ case bool:
+ if arg {
+ b.value = 1
+ }
+ case *Unsigned16:
+ b.value = arg.value
+ case string:
+ switch arg {
+ case "True", "true":
+ b.value = 1
+ case "False", "false":
+ default:
+ return nil, errors.Errorf("invalid string: %s", arg)
+ }
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return b, nil
+}
+
+func (b *Unsigned16) Encode(tag *Tag) {
+ tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_UNSIGNED_INTEGER, b.value, []byte{}))
+}
+
+func (b *Unsigned16) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_UNSIGNED_INTEGER) {
+ return errors.New("Unsigned16 application tag required")
+ }
+ if tag.tagLVT > 1 {
+ return errors.New("invalid tag value")
+ }
+
+ // get the data
+ if tag.tagLVT == 1 {
+ b.value = 1
+ }
+ return nil
+}
+
+func (b *Unsigned16) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (b *Unsigned16) String() string {
+ value := "False"
+ if b.value == 1 {
+ value = "True"
+ }
+ return fmt.Sprintf("Unsigned16(%s)", value)
+}
+
+// TODO: finish
+type Integer struct {
+ *Atomic[int]
+ *CommonMath
+}
+
+func NewInteger(arg Arg) (*Integer, error) {
+ b := &Integer{}
+ b.Atomic = NewAtomic[int](b)
+
+ if arg == nil {
+ return b, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := b.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return b, nil
+ case bool:
+ if arg {
+ b.value = 1
+ }
+ case *Integer:
+ b.value = arg.value
+ case string:
+ switch arg {
+ case "True", "true":
+ b.value = 1
+ case "False", "false":
+ default:
+ return nil, errors.Errorf("invalid string: %s", arg)
+ }
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return b, nil
+}
+
+func (b *Integer) Encode(tag *Tag) {
+ tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_UNSIGNED_INTEGER, b.value, []byte{}))
+}
+
+func (b *Integer) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_UNSIGNED_INTEGER) {
+ return errors.New("Integer application tag required")
+ }
+ if tag.tagLVT > 1 {
+ return errors.New("invalid tag value")
+ }
+
+ // get the data
+ if tag.tagLVT == 1 {
+ b.value = 1
+ }
+ return nil
+}
+
+func (b *Integer) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (b *Integer) String() string {
+ value := "False"
+ if b.value == 1 {
+ value = "True"
+ }
+ return fmt.Sprintf("Integer(%s)", value)
+}
+
+// TODO: finish
+type Real struct {
+ *Atomic[float32]
+ *CommonMath
+}
+
+func NewReal(arg Arg) (*Real, error) {
+ b := &Real{}
+ b.Atomic = NewAtomic[float32](b)
+
+ if arg == nil {
+ return b, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := b.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return b, nil
+ case bool:
+ if arg {
+ b.value = 1
+ }
+ case *Real:
+ b.value = arg.value
+ case string:
+ switch arg {
+ case "True", "true":
+ b.value = 1
+ case "False", "false":
+ default:
+ return nil, errors.Errorf("invalid string: %s", arg)
+ }
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return b, nil
+}
+
+func (b *Real) Encode(tag *Tag) {
+ tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_REAL, b.value, []byte{}))
+}
+
+func (b *Real) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_REAL) {
+ return errors.New("Real application tag required")
+ }
+ if tag.tagLVT > 1 {
+ return errors.New("invalid tag value")
+ }
+
+ // get the data
+ if tag.tagLVT == 1 {
+ b.value = 1
+ }
+ return nil
+}
+
+func (b *Real) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (b *Real) String() string {
+ value := "False"
+ if b.value == 1 {
+ value = "True"
+ }
+ return fmt.Sprintf("Real(%s)", value)
+}
+
+// TODO: finish
+type Double struct {
+ *Atomic[float64]
+ *CommonMath
+}
+
+func NewDouble(arg Arg) (*Double, error) {
+ b := &Double{}
+ b.Atomic = NewAtomic[float64](b)
+
+ if arg == nil {
+ return b, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := b.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return b, nil
+ case bool:
+ if arg {
+ b.value = 1
+ }
+ case *Double:
+ b.value = arg.value
+ case string:
+ switch arg {
+ case "True", "true":
+ b.value = 1
+ case "False", "false":
+ default:
+ return nil, errors.Errorf("invalid string: %s", arg)
+ }
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return b, nil
+}
+
+func (b *Double) Encode(tag *Tag) {
+ tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_DOUBLE, b.value, []byte{}))
+}
+
+func (b *Double) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_DOUBLE) {
+ return errors.New("Double application tag required")
+ }
+ if tag.tagLVT > 1 {
+ return errors.New("invalid tag value")
+ }
+
+ // get the data
+ if tag.tagLVT == 1 {
+ b.value = 1
+ }
+ return nil
+}
+
+func (b *Double) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (b *Double) String() string {
+ value := "False"
+ if b.value == 1 {
+ value = "True"
+ }
+ return fmt.Sprintf("Double(%s)", value)
+}
+
+// TODO: finish
+type OctetString struct {
+ *Atomic[string]
+ *CommonMath
+}
+
+func NewOctetString(arg Arg) (*OctetString, error) {
+ b := &OctetString{}
+ b.Atomic = NewAtomic[string](b)
+
+ if arg == nil {
+ return b, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := b.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return b, nil
+ case bool:
+ if arg {
+ b.value = "1"
+ }
+ case *OctetString:
+ b.value = arg.value
+ case string:
+ switch arg {
+ case "True", "true":
+ b.value = "1"
+ case "False", "false":
+ default:
+ return nil, errors.Errorf("invalid string: %s", arg)
+ }
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return b, nil
+}
+
+func (b *OctetString) Encode(tag *Tag) {
+ tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_OCTET_STRING, b.value, []byte{}))
+}
+
+func (b *OctetString) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_OCTET_STRING) {
+ return errors.New("OctetString application tag required")
+ }
+ if tag.tagLVT > 1 {
+ return errors.New("invalid tag value")
+ }
+
+ // get the data
+ if tag.tagLVT == 1 {
+ b.value = "1"
+ }
+ return nil
+}
+
+func (b *OctetString) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (b *OctetString) String() string {
+ value := "False"
+ if b.value == "1" {
+ value = "True"
+ }
+ return fmt.Sprintf("OctetString(%s)", value)
+}
+
+type CharacterString struct {
+ *Atomic[string]
+ *CommonMath
+
+ strEncoding byte
+ strValue []byte
+}
+
+func NewCharacterString(arg Arg) (*CharacterString, error) {
+ c := &CharacterString{}
+ c.Atomic = NewAtomic[string](c)
+
+ if arg == nil {
+ return c, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := c.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return c, nil
+ case string:
+ c.value = arg
+ c.strValue = []byte(c.value)
+ case *CharacterString:
+ c.value = arg.value
+ c.strEncoding = arg.strEncoding
+ c.strValue = arg.strValue
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return c, nil
+}
+
+func (c *CharacterString) Encode(tag *Tag) {
+ tag.setAppData(uint(model.BACnetDataType_CHARACTER_STRING), append([]byte{c.strEncoding}, c.strValue...))
+}
+
+func (c *CharacterString) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_CHARACTER_STRING) {
+ return errors.New("CharacterString application tag required")
+ }
+ if len(tag.tagData) == 0 {
+ return errors.New("invalid tag length")
+ }
+
+ tagData := tag.tagData
+
+ // extract the data
+ c.strEncoding = tagData[0]
+ c.strValue = tagData[1:]
+
+ // normalize the value
+ switch c.strEncoding {
+ case 0:
+ c.value = string(c.strValue)
+ case 3: //utf_32be
+ panic("implement me") // TODO: implement me
+ case 4: //utf_16be
+ panic("implement me") // TODO: implement me
+ case 5: //latin_1
+ panic("implement me") // TODO: implement me
+ default:
+ c.value = fmt.Sprintf("### unknown encoding: %d ###", c.strEncoding)
+ }
+
+ return nil
+}
+
+func (c *CharacterString) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (c *CharacterString) String() string {
+ return fmt.Sprintf("CharacterString(%d,X'%s')", c.strEncoding, Btox(c.strValue))
+}
+
// BitStringExtension can be used to inherit from BitString
type BitStringExtension interface {
fmt.Stringer
@@ -495,3 +1155,408 @@
// bundle it together
return fmt.Sprintf("BitString(%v)", strings.Join(valueList, ","))
}
+
+// TODO: implement me
+type Enumerated struct {
+ *Atomic[int] // TODO: implement properly
+
+ Value []bool
+}
+
+func NewEnumerated(arg ...any) (*Enumerated, error) {
+ panic("not implemented")
+}
+
+func (b *Enumerated) Decode(tag *Tag) error {
+ if tag.GetTagClass() != model.TagClass_APPLICATION_TAGS || tag.GetTagNumber() != uint(TagEnumeratedAppTag) {
+ return errors.New("bit string application tag required")
+ }
+ if len(tag.GetTagData()) == 0 {
+ return errors.New("invalid tag length")
+ }
+ // extract the number of unused bits
+ unused := tag.tagData[0]
+
+ // extract the data
+ data := make([]bool, 0)
+ for _, x := range tag.tagData[1:] {
+ for i := range 8 {
+ if (x & (1 << (7 - i))) != 0 {
+ data = append(data, true)
+ } else {
+ data = append(data, false)
+ }
+ }
+ }
+
+ // trim off the unused bits
+ if unused != 0 && unused != 8 {
+ b.Value = data[:len(data)-int(unused)]
+ } else {
+ b.Value = data
+ }
+ return nil
+}
+
+func (b *Enumerated) Encode(tag *Tag) {
+ used := len(b.Value) % 8
+ unused := 8 - used
+ if unused == 8 {
+ unused = 0
+ }
+
+ // start with the number of unused bits
+ data := []byte{byte(unused)}
+
+ // build and append each packed octet
+ bits := append(b.Value, make([]bool, unused)...)
+ for i := range len(bits) / 8 {
+ i = i * 8
+ x := byte(0)
+ for j := range 8 {
+ bit := bits[i+j]
+ bitValue := byte(0)
+ if bit {
+ bitValue = 1
+ }
+ x |= bitValue << (7 - j)
+ }
+ data = append(data, x)
+ }
+
+ tag.setAppData(uint(model.BACnetDataType_BIT_STRING), data)
+}
+
+func (b *Enumerated) String() string {
+ // flip the bit names
+ bitNames := map[int]string{}
+
+ // build a list of values and/or names
+ var valueList []string
+ for index, value := range b.Value {
+ if name, ok := bitNames[index]; ok {
+ if value == true {
+ valueList = append(valueList, name)
+ } else {
+ valueList = append(valueList, "!"+name)
+ }
+ } else {
+ if value {
+ valueList = append(valueList, "1")
+ } else {
+ valueList = append(valueList, "0")
+ }
+ }
+ }
+
+ // bundle it together
+ return fmt.Sprintf("Enumerated(%v)", strings.Join(valueList, ","))
+}
+
+const _mm = `(?P<month>0?[1-9]|1[0-4]|odd|even|255|[*])`
+const _dd = `(?P<day>[0-3]?\d|last|odd|even|255|[*])`
+const _yy = `(?P<year>\d{2}|255|[*])`
+const _yyyy = `(?P<year>\d{4}|255|[*])`
+const _dow = `(?P<dow>[1-7]|mon|tue|wed|thu|fri|sat|sun|255|[*])`
+
+var _special_mon = map[string]int{"*": 255, "odd": 13, "even": 14, "": 255}
+var _special_mon_inv = map[int]string{255: "*", 13: "odd", 14: "even"}
+
+var _special_day = map[string]int{"*": 255, "last": 32, "odd": 33, "even": 34, "": 255}
+var _special_day_inv = map[int]string{255: "*", 32: "last", 33: "odd", 34: "even"}
+
+var _special_dow = map[string]int{"*": 255, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6, "sun": 7}
+var _special_dow_inv = map[int]string{255: "*", 1: "mon", 2: "tue", 3: "wed", 4: "thu", 5: "fri", 6: "sat", 7: "sun"}
+
+// Create a composite pattern and compile it.
+func _merge(args ...string) *regexp.Regexp {
+ return regexp.MustCompile(`^` + strings.Join(args, `[/-]`) + `(?:\s+` + _dow + `)?$`)
+}
+
+// make a list of compiled patterns
+var _date_patterns = []*regexp.Regexp{
+ _merge(_yyyy, _mm, _dd),
+ _merge(_mm, _dd, _yyyy),
+ _merge(_dd, _mm, _yyyy),
+ _merge(_yy, _mm, _dd),
+ _merge(_mm, _dd, _yy),
+ _merge(_dd, _mm, _yy),
+}
+
+type DateTuple struct {
+ Year int
+ Month int
+ Day int
+ DayOfWeek int
+}
+
+type Date struct {
+ *Atomic[int64]
+
+ year int
+ month int
+ day int
+ dayOfWeek int
+}
+
+func NewDate(arg Arg, args Args) (*Date, error) {
+ d := &Date{}
+ d.Atomic = NewAtomic[int64](d)
+ year := 255
+ if len(args) > 0 {
+ year = args[0].(int)
+ }
+ if year >= 1900 {
+ year = year - 1900
+ }
+ d.year = year
+ month := 0xff
+ if len(args) > 1 {
+ month = args[1].(int)
+ }
+ d.month = month
+ day := 0xff
+ if len(args) > 2 {
+ day = args[2].(int)
+ }
+ d.day = day
+ dayOfWeek := 0xff
+ if len(args) > 3 {
+ dayOfWeek = args[3].(int)
+ }
+ d.dayOfWeek = dayOfWeek
+
+ if arg == nil {
+ return d, nil
+ }
+ switch arg := arg.(type) {
+ case *Tag:
+ err := d.Decode(arg)
+ if err != nil {
+ return nil, errors.Wrap(err, "error decoding")
+ }
+ return d, nil
+ case DateTuple:
+ d.year, d.month, d.day, d.dayOfWeek = arg.Year, arg.Month, arg.Day, arg.DayOfWeek
+ var tempTime time.Time
+ tempTime.AddDate(d.year, d.month, d.day)
+ d.value = tempTime.UnixNano() - (time.Time{}.UnixNano()) // TODO: check this
+ case string:
+ // lower case everything
+ arg = strings.ToLower(arg)
+
+ // make a list of the contents from matching patterns
+ matches := [][]string{}
+ for _, p := range _date_patterns {
+ if p.MatchString(arg) {
+ groups := combined_pattern.FindStringSubmatch(arg)
+ matches = append(matches, groups[1:])
+ }
+ }
+ if len(matches) == 0 {
+ return nil, errors.New("unmatched")
+ }
+
+ var match []string
+ if len(matches) == 1 {
+ match = matches[0]
+ } else {
+ // check to see if they really are the same
+ panic("what to do here")
+ }
+
+ // extract the year and normalize
+ matchedYear := match[0]
+ if matchedYear == "*" || matchedYear == "" {
+ year = 0xff
+ } else {
+ yearParse, err := strconv.ParseInt(matchedYear, 10, 64)
+ if err != nil {
+ return nil, errors.Wrap(err, "error parsing year")
+ }
+ year = int(yearParse)
+ if year == 0xff {
+ return d, nil
+ }
+ if year < 35 {
+ year += 2000
+ } else if year < 100 {
+ year += 1900
+ } else if year < 1900 {
+ return nil, errors.New("invalid year")
+ }
+ }
+
+ // extract the month and normalize
+ matchedmonth := match[0]
+ if specialMonth, ok := _special_mon[matchedmonth]; ok {
+ month = specialMonth
+ } else {
+ monthParse, err := strconv.ParseInt(matchedmonth, 10, 64)
+ if err != nil {
+ return nil, errors.Wrap(err, "error parsing month")
+ }
+ month = int(monthParse)
+ if month == 0xff {
+ return d, nil
+ }
+ if month == 0 || month > 14 {
+ return nil, errors.New("invalid month")
+ }
+ }
+
+ // extract the day and normalize
+ matchedday := match[0]
+ if specialday, ok := _special_day[matchedday]; ok {
+ day = specialday
+ } else {
+ dayParse, err := strconv.ParseInt(matchedday, 10, 64)
+ if err != nil {
+ return nil, errors.Wrap(err, "error parsing day")
+ }
+ day = int(dayParse)
+ if day == 0xff {
+ return d, nil
+ }
+ if day == 0 || day > 34 {
+ return nil, errors.New("invalid day")
+ }
+ }
+
+ // extract the dayOfWeek and normalize
+ matcheddayOfWeek := match[0]
+ if specialdayOfWeek, ok := _special_dow[matcheddayOfWeek]; ok {
+ dayOfWeek = specialdayOfWeek
+ } else if matcheddayOfWeek == "" {
+ return d, nil
+ } else {
+ dayOfWeekParse, err := strconv.ParseInt(matcheddayOfWeek, 10, 64)
+ if err != nil {
+ return nil, errors.Wrap(err, "error parsing dayOfWeek")
+ }
+ dayOfWeek = int(dayOfWeekParse)
+ if dayOfWeek == 0xff {
+ return d, nil
+ }
+ if dayOfWeek > 7 {
+ return nil, errors.New("invalid dayOfWeek")
+ }
+ }
+
+ // year becomes the correct octet
+ if year != 0xff {
+ year -= 1900
+ }
+
+ // save the value
+ d.year = year
+ d.month = month
+ d.day = day
+ d.dayOfWeek = dayOfWeek
+
+ var tempTime time.Time
+ tempTime.AddDate(year, month, day)
+ d.value = tempTime.UnixNano() - (time.Time{}.UnixNano()) // TODO: check this
+
+ // calculate the day of the week
+ if dayOfWeek == 0 {
+ d.calcDayOfWeek()
+ }
+ case *Date:
+ d.value = arg.value
+ d.year = arg.year
+ d.month = arg.month
+ d.day = arg.day
+ d.dayOfWeek = arg.dayOfWeek
+ case float32:
+ d.now(arg)
+ default:
+ return nil, errors.Errorf("invalid constructor datatype: %T", arg)
+ }
+
+ return d, nil
+}
+
+func (d *Date) GetTupleValue() (year int, month int, day int, dayOfWeek int) {
+ return d.year, d.month, d.day, d.dayOfWeek
+}
+
+func (d *Date) calcDayOfWeek() {
+ year, month, day, dayOfWeek := d.year, d.month, d.day, d.dayOfWeek
+
+ // assume the worst
+ dayOfWeek = 255
+
+ // check for special values
+ if year == 255 {
+ return
+ } else if _, ok := _special_mon_inv[month]; ok {
+ return
+ } else if _, ok := _special_day_inv[month]; ok {
+ return
+ } else {
+ var today time.Time
+ today = time.Date(year+1900, time.Month(month), day, 0, 0, 0, 0, time.UTC)
+ panic(today) // TODO: implement me
+ }
+
+ // put it back together
+ d.year = year
+ d.month = month
+ d.day = day
+ d.dayOfWeek = dayOfWeek
+}
+
+func (d *Date) now(arg float32) {
+ panic("implement me") // TODO
+}
+
+func (d *Date) Encode(tag *Tag) {
+ var b []byte
+ binary.BigEndian.AppendUint64(b, uint64(d.value))
+ tag.setAppData(uint(model.BACnetDataType_DATE), b)
+}
+
+func (d *Date) Decode(tag *Tag) error {
+ if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_DATE) {
+ return errors.New("Date application tag required")
+ }
+ if len(tag.tagData) != 4 {
+ return errors.New("invalid tag length")
+ }
+
+ arg := tag.tagData
+ year, month, day, dayOfWeek := arg[0], arg[1], arg[2], arg[3]
+ var tempTime time.Time
+ tempTime.AddDate(int(year), int(month), int(day))
+ d.value = tempTime.UnixNano() - (time.Time{}.UnixNano()) // TODO: check this
+ d.year, d.month, d.day, d.dayOfWeek = int(year), int(month), int(day), int(dayOfWeek)
+ return nil
+}
+
+func (d *Date) IsValid(arg any) bool {
+ _, ok := arg.(bool)
+ return ok
+}
+
+func (d *Date) String() string {
+ year, month, day, dayOfWeek := d.year, d.month, d.day, d.dayOfWeek
+ yearStr := "*"
+ if year != 255 {
+ yearStr = strconv.Itoa(year + 1900)
+ }
+ monthStr := strconv.Itoa(month)
+ if ms, ok := _special_mon_inv[month]; ok {
+ monthStr = ms
+ }
+ dayStr := strconv.Itoa(day)
+ if ms, ok := _special_day_inv[day]; ok {
+ dayStr = ms
+ }
+ dowStr := strconv.Itoa(dayOfWeek)
+ if ms, ok := _special_dow_inv[dayOfWeek]; ok {
+ dowStr = ms
+ }
+
+ return fmt.Sprintf("Date(%s-%s-%s %s)", yearStr, monthStr, dayStr, dowStr)
+}
diff --git a/plc4go/internal/bacnetip/tests/test_primitive_data/test_character_string_test.go b/plc4go/internal/bacnetip/tests/test_primitive_data/test_character_string_test.go
index ed516b5..7fa35a5 100644
--- a/plc4go/internal/bacnetip/tests/test_primitive_data/test_character_string_test.go
+++ b/plc4go/internal/bacnetip/tests/test_primitive_data/test_character_string_test.go
@@ -19,4 +19,134 @@
package test_primitive_data
-// TODO: implement me
+import (
+ "github.com/stretchr/testify/require"
+ "testing"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const foxMessage = "the quick brown fox jumped over the lazy dog"
+
+func CharacterString(arg ...any) *bacnetip.CharacterString {
+ if len(arg) == 0 {
+ CharacterString, err := bacnetip.NewCharacterString(nil)
+ if err != nil {
+ panic(err)
+ }
+ return CharacterString
+ }
+ CharacterString, err := bacnetip.NewCharacterString(arg[0])
+ if err != nil {
+ panic(err)
+ }
+ return CharacterString
+}
+
+// Convert a hex string to a character_string application tag.
+func CharacterStringTag(x string) *bacnetip.Tag {
+ b := xtob(x)
+ tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_CHARACTER_STRING, len(b), b)
+ return tag
+}
+
+// Encode a CharacterString object into a tag.
+func CharacterStringEncode(obj *bacnetip.CharacterString) *bacnetip.Tag {
+ tag := Tag()
+ obj.Encode(tag)
+ return tag
+}
+
+// Decode a CharacterString application tag into a CharacterString.
+func CharacterStringDecode(tag *bacnetip.Tag) *bacnetip.CharacterString {
+ obj := CharacterString(tag)
+
+ return obj
+}
+
+// Pass the value to CharacterString, construct a tag from the hex string,
+//
+// and compare results of encode and decoding each other.
+func CharacterStringEndec(t *testing.T, v string, x string) {
+ tag := CharacterStringTag(x)
+
+ obj := CharacterString(v)
+
+ assert.Equal(t, tag, CharacterStringEncode(obj))
+ assert.Equal(t, obj, CharacterStringDecode(tag))
+}
+
+func TestCharacterString(t *testing.T) {
+ obj := CharacterString()
+ assert.Equal(t, "", obj.GetValue())
+
+ assert.Panics(t, func() {
+ CharacterString(1)
+ })
+ assert.Panics(t, func() {
+ CharacterString(1.0)
+ })
+}
+
+func TestCharacterStringStr(t *testing.T) {
+ obj := CharacterString("hello")
+ assert.Equal(t, "hello", obj.GetValue())
+ assert.Equal(t, "CharacterString(0,X'68656c6c6f')", obj.String())
+}
+
+func TestCharacterStringStrUnicode(t *testing.T) {
+ obj := CharacterString("hello")
+ assert.Equal(t, "hello", obj.GetValue())
+ assert.Equal(t, "CharacterString(0,X'68656c6c6f')", obj.String())
+}
+
+func TestCharacterStringStrUnicodeWithLatin(t *testing.T) {
+ // some controllers encoding character string mixing latin-1 and utf-8
+ // try to cover those cases without failing
+ b := xtob("0030b043") // zero degress celsius
+ tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_CHARACTER_STRING, len(b), b)
+ obj := CharacterString()
+ err := obj.Decode(tag)
+ require.NoError(t, err)
+ assert.Equal(t, "CharacterString(0,X'30b043')", obj.String())
+
+ assert.Equal(t, "0\xb0C", obj.GetValue())
+}
+
+func TestCharacterStringTag(t *testing.T) {
+ tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_CHARACTER_STRING, 1, xtob("00"))
+ obj := CharacterString(tag)
+ assert.Equal(t, obj.GetValue(), "")
+
+ tag = Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_BOOLEAN, 0, xtob(""))
+ assert.Panics(t, func() {
+ CharacterString(tag)
+ })
+
+ tag = Tag(model.TagClass_CONTEXT_SPECIFIC_TAGS, 0, 1, xtob("ff"))
+ assert.Panics(t, func() {
+ CharacterString(tag)
+ })
+
+ tag = Tag(bacnetip.TagOpeningTagClass, 0)
+ assert.Panics(t, func() {
+ CharacterString(tag)
+ })
+}
+
+func TestCharacterStringCopy(t *testing.T) {
+ obj1 := CharacterString(foxMessage)
+ obj2 := CharacterString(obj1)
+ assert.Equal(t, obj1, obj2)
+}
+
+func TestCharacterStringEndec(t *testing.T) {
+ assert.Panics(t, func() {
+ CharacterString(CharacterStringTag(""))
+ })
+ CharacterStringEndec(t, "", "00")
+ CharacterStringEndec(t, "abc", "00616263")
+}
diff --git a/plc4go/internal/bacnetip/tests/test_primitive_data/test_date_test.go b/plc4go/internal/bacnetip/tests/test_primitive_data/test_date_test.go
index ed516b5..425ce45 100644
--- a/plc4go/internal/bacnetip/tests/test_primitive_data/test_date_test.go
+++ b/plc4go/internal/bacnetip/tests/test_primitive_data/test_date_test.go
@@ -19,4 +19,123 @@
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"
+)
+
+func Date(arg ...any) *bacnetip.Date {
+ if len(arg) == 0 {
+ Date, err := bacnetip.NewDate(nil, nil)
+ if err != nil {
+ panic(err)
+ }
+ return Date
+ }
+ Date, err := bacnetip.NewDate(arg[0], nil)
+ if err != nil {
+ panic(err)
+ }
+ return Date
+}
+
+// Convert a hex string to a character_string application tag.
+func DateTag(x string) *bacnetip.Tag {
+ b := xtob(x)
+ tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_DATE, len(b), b)
+ return tag
+}
+
+// Encode a Date object into a tag.
+func DateEncode(obj *bacnetip.Date) *bacnetip.Tag {
+ tag := Tag()
+ obj.Encode(tag)
+ return tag
+}
+
+// Decode a Date application tag into a Date.
+func DateDecode(tag *bacnetip.Tag) *bacnetip.Date {
+ obj := Date(tag)
+
+ return obj
+}
+
+// Pass the value to Date, construct a tag from the hex string,
+//
+// and compare results of encode and decoding each other.
+func DateEndec(t *testing.T, v string, x string) {
+ tag := DateTag(x)
+
+ obj := Date(v)
+
+ assert.Equal(t, tag, DateEncode(obj))
+ assert.Equal(t, obj, DateDecode(tag))
+}
+
+func TestDate(t *testing.T) {
+ obj := Date()
+ assert.Equal(t, int64(0), obj.GetValue())
+ year, month, day, week := obj.GetTupleValue()
+ assert.Equal(t, []any{0xff, 0xff, 0xff, 0xff}, []any{year, month, day, week})
+
+ assert.Panics(t, func() {
+ Date("some string")
+ })
+}
+
+func TestDateTuple(t *testing.T) {
+ obj := Date(bacnetip.DateTuple{Year: 1, Month: 2, Day: 3, DayOfWeek: 4})
+ year, month, day, week := obj.GetTupleValue()
+ assert.Equal(t, []any{1, 2, 3, 4}, []any{year, month, day, week})
+ assert.Equal(t, "Date(1901-2-3 thu)", obj.String())
+}
+
+func TestDateTag(t *testing.T) {
+ tag := Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_DATE, 4, xtob("01020304"))
+ obj := Date(tag)
+ year, month, day, week := obj.GetTupleValue()
+ assert.Equal(t, []any{1, 2, 3, 4}, []any{year, month, day, week})
+
+ tag = Tag(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_BOOLEAN, 0, xtob(""))
+ assert.Panics(t, func() {
+ Date(tag)
+ })
+
+ tag = Tag(model.TagClass_CONTEXT_SPECIFIC_TAGS, 0, 1, xtob("ff"))
+ assert.Panics(t, func() {
+ Date(tag)
+ })
+
+ tag = Tag(bacnetip.TagOpeningTagClass, 0)
+ assert.Panics(t, func() {
+ Date(tag)
+ })
+}
+
+func TestDateCopy(t *testing.T) {
+ obj1 := Date(bacnetip.DateTuple{Year: 1, Month: 2, Day: 3, DayOfWeek: 4})
+ obj2 := Date(obj1)
+ assert.Equal(t, obj1, obj2)
+}
+
+func TestDateNow(t *testing.T) {
+ // TODO: upstream doesn't tests this either
+}
+
+func TestDateEndec(t *testing.T) {
+ assert.Panics(t, func() {
+ Date(DateTag(""))
+ })
+}
+
+func TestDateArgs(t *testing.T) {
+ tag := Tag()
+ date := Date(nil, 2023, 2, 10)
+ date1 := Date(nil, 123, 2, 10)
+ assert.Equal(t, date, date1)
+ date.Encode(tag)
+}