diff --git a/plc4go/internal/bacnetip/debugging.go b/plc4go/internal/bacnetip/debugging.go index 5c11427494f..0136cff3c8e 100644 --- a/plc4go/internal/bacnetip/debugging.go +++ b/plc4go/internal/bacnetip/debugging.go @@ -24,6 +24,10 @@ import ( "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 3d76709a0c4..398ac56b245 100644 --- a/plc4go/internal/bacnetip/primitivedata.go +++ b/plc4go/internal/bacnetip/primitivedata.go @@ -22,8 +22,12 @@ package bacnetip import ( "bytes" "cmp" + "encoding/binary" "fmt" + "regexp" + "strconv" "strings" + "time" "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model" @@ -256,18 +260,603 @@ type CommonMath struct { // 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 { + *Atomic[int] //Note we need int as bool can't be used +} + +func NewBoolean(arg Arg) (*Boolean, error) { + b := &Boolean{} + 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 *Boolean: + 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 *Boolean) Encode(tag *Tag) { + tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_BOOLEAN, b.value, []byte{})) +} + +func (b *Boolean) Decode(tag *Tag) error { + if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_BOOLEAN) { + return errors.New("boolean 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 *Boolean) IsValid(arg any) bool { + _, ok := arg.(bool) + return ok +} + +// GetValue gives an int value because bool can't be used in constraint. A convenience method GetBoolValue exists. +func (b *Boolean) GetValue() int { + return b.Atomic.GetValue() +} + +func (b *Boolean) GetBoolValue() bool { + return b.GetValue() == 1 +} + +func (b *Boolean) String() string { + value := "False" + if b.value == 1 { + value = "True" + } + 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) } -type Boolean struct { - *Atomic[int] //Note we need int as bool can't be used +// TODO: finish +type OctetString struct { + *Atomic[string] + *CommonMath } -func NewBoolean(arg Arg) (*Boolean, error) { - b := &Boolean{} - b.Atomic = NewAtomic[int](b) - b.value = 0 // atomic doesn't like bool +func NewOctetString(arg Arg) (*OctetString, error) { + b := &OctetString{} + b.Atomic = NewAtomic[string](b) if arg == nil { return b, nil @@ -281,14 +870,14 @@ func NewBoolean(arg Arg) (*Boolean, error) { return b, nil case bool: if arg { - b.value = 1 + b.value = "1" } - case *Boolean: + case *OctetString: b.value = arg.value case string: switch arg { case "True", "true": - b.value = 1 + b.value = "1" case "False", "false": default: return nil, errors.Errorf("invalid string: %s", arg) @@ -300,13 +889,13 @@ func NewBoolean(arg Arg) (*Boolean, error) { return b, nil } -func (b *Boolean) Encode(tag *Tag) { - tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_BOOLEAN, b.value, []byte{})) +func (b *OctetString) Encode(tag *Tag) { + tag.set(NewArgs(model.TagClass_APPLICATION_TAGS, model.BACnetDataType_OCTET_STRING, b.value, []byte{})) } -func (b *Boolean) Decode(tag *Tag) error { - if tag.tagClass != model.TagClass_APPLICATION_TAGS || tag.tagNumber != uint(model.BACnetDataType_BOOLEAN) { - return errors.New("boolean application tag required") +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") @@ -314,31 +903,102 @@ func (b *Boolean) Decode(tag *Tag) error { // get the data if tag.tagLVT == 1 { - b.value = 1 + b.value = "1" } return nil } -func (b *Boolean) IsValid(arg any) bool { +func (b *OctetString) IsValid(arg any) bool { _, ok := arg.(bool) return ok } -// GetValue gives an int value because bool can't be used in constraint. A convenience method GetBoolValue exists. -func (b *Boolean) GetValue() int { - return b.Atomic.GetValue() +func (b *OctetString) String() string { + value := "False" + if b.value == "1" { + value = "True" + } + return fmt.Sprintf("OctetString(%s)", value) } -func (b *Boolean) GetBoolValue() bool { - return b.GetValue() == 1 +type CharacterString struct { + *Atomic[string] + *CommonMath + + strEncoding byte + strValue []byte } -func (b *Boolean) String() string { - value := "False" - if b.value == 1 { - value = "True" +func NewCharacterString(arg Arg) (*CharacterString, error) { + c := &CharacterString{} + c.Atomic = NewAtomic[string](c) + + if arg == nil { + return c, nil } - return fmt.Sprintf("Boolean(%s)", value) + 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 @@ -495,3 +1155,408 @@ func (b *BitString) String() string { // 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 = `(?P0?[1-9]|1[0-4]|odd|even|255|[*])` +const _dd = `(?P[0-3]?\d|last|odd|even|255|[*])` +const _yy = `(?P\d{2}|255|[*])` +const _yyyy = `(?P\d{4}|255|[*])` +const _dow = `(?P[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 ed516b50110..7fa35a559b0 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 ed516b50110..425ce4590c8 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) +}