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 2021/03/29 14:07:28 UTC

[plc4x] branch develop updated: plc4go: first ads driver draft + contains additional minor tweaks

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


The following commit(s) were added to refs/heads/develop by this push:
     new 37fb82c  plc4go: first ads driver draft + contains additional minor tweaks
37fb82c is described below

commit 37fb82cde0728923282628118adb47d9f54d190c
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Mon Mar 29 16:07:11 2021 +0200

    plc4go: first ads driver draft
    + contains additional minor tweaks
---
 plc4go/internal/plc4go/ads/Connection.go           | 157 +++++++++++++++
 plc4go/internal/plc4go/ads/Driver.go               |  49 ++++-
 plc4go/internal/plc4go/ads/Field.go                | 113 +++++++++++
 plc4go/internal/plc4go/ads/FieldHandler.go         | 134 ++++++++++++-
 plc4go/internal/plc4go/ads/MessageCodec.go         | 217 +++++++++++++++++++++
 plc4go/internal/plc4go/ads/Reader.go               | 217 +++++++++++++++++++++
 .../ads/{FieldHandler.go => ValueHandler.go}       |  16 +-
 plc4go/internal/plc4go/ads/Writer.go               | 200 +++++++++++++++++++
 plc4go/internal/plc4go/ads/fieldtype_string.go     |  54 +++++
 plc4go/internal/plc4go/modbus/FieldHandler.go      |   4 +-
 .../plc4go/spi/testutils/DriverTestRunner.go       |  50 ++++-
 plc4go/pkg/plc4go/driverManager.go                 |  10 +-
 plc4go/pom.xml                                     |  16 ++
 13 files changed, 1203 insertions(+), 34 deletions(-)

diff --git a/plc4go/internal/plc4go/ads/Connection.go b/plc4go/internal/plc4go/ads/Connection.go
new file mode 100644
index 0000000..4ffefcc
--- /dev/null
+++ b/plc4go/internal/plc4go/ads/Connection.go
@@ -0,0 +1,157 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+package ads
+
+import (
+	"fmt"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/interceptors"
+	internalModel "github.com/apache/plc4x/plc4go/internal/plc4go/spi/model"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/transports"
+	"github.com/apache/plc4x/plc4go/pkg/plc4go"
+	apiModel "github.com/apache/plc4x/plc4go/pkg/plc4go/model"
+	"github.com/rs/zerolog/log"
+	"time"
+)
+
+type ConnectionMetadata struct {
+}
+
+func (m ConnectionMetadata) GetConnectionAttributes() map[string]string {
+	return map[string]string{}
+}
+
+func (m ConnectionMetadata) CanRead() bool {
+	return true
+}
+
+func (m ConnectionMetadata) CanWrite() bool {
+	return true
+}
+
+func (m ConnectionMetadata) CanSubscribe() bool {
+	return true
+}
+
+func (m ConnectionMetadata) CanBrowse() bool {
+	return false
+}
+
+// TODO: maybe we can use a DefaultConnection struct here with delegates
+type Connection struct {
+	messageCodec       spi.MessageCodec
+	options            map[string][]string
+	fieldHandler       spi.PlcFieldHandler
+	valueHandler       spi.PlcValueHandler
+	requestInterceptor internalModel.RequestInterceptor
+}
+
+func NewConnection(messageCodec spi.MessageCodec, options map[string][]string, fieldHandler spi.PlcFieldHandler) Connection {
+	return Connection{
+		messageCodec:       messageCodec,
+		options:            options,
+		fieldHandler:       fieldHandler,
+		valueHandler:       NewValueHandler(),
+		requestInterceptor: interceptors.NewSingleItemRequestInterceptor(),
+	}
+}
+
+func (m Connection) Connect() <-chan plc4go.PlcConnectionConnectResult {
+	log.Trace().Msg("Connecting")
+	ch := make(chan plc4go.PlcConnectionConnectResult)
+	go func() {
+		err := m.messageCodec.Connect()
+		ch <- plc4go.NewPlcConnectionConnectResult(m, err)
+	}()
+	return ch
+}
+
+func (m Connection) BlockingClose() {
+	log.Trace().Msg("Closing blocked")
+	closeResults := m.Close()
+	select {
+	case <-closeResults:
+		return
+	case <-time.After(time.Second * 5):
+		return
+	}
+}
+
+func (m Connection) Close() <-chan plc4go.PlcConnectionCloseResult {
+	log.Trace().Msg("Close")
+	// TODO: Implement ...
+	ch := make(chan plc4go.PlcConnectionCloseResult)
+	go func() {
+		ch <- plc4go.NewPlcConnectionCloseResult(m, nil)
+	}()
+	return ch
+}
+
+func (m Connection) IsConnected() bool {
+	panic("implement me")
+}
+
+func (m Connection) Ping() <-chan plc4go.PlcConnectionPingResult {
+	panic("implement me")
+}
+
+func (m Connection) GetMetadata() apiModel.PlcConnectionMetadata {
+	return ConnectionMetadata{}
+}
+
+func (m Connection) ReadRequestBuilder() apiModel.PlcReadRequestBuilder {
+	return internalModel.NewDefaultPlcReadRequestBuilderWithInterceptor(m.fieldHandler,
+		NewReader(m.messageCodec), m.requestInterceptor)
+}
+
+func (m Connection) WriteRequestBuilder() apiModel.PlcWriteRequestBuilder {
+	return internalModel.NewDefaultPlcWriteRequestBuilder(
+		m.fieldHandler, m.valueHandler, NewWriter(m.messageCodec))
+}
+
+func (m Connection) SubscriptionRequestBuilder() apiModel.PlcSubscriptionRequestBuilder {
+	panic("implement me")
+}
+
+func (m Connection) UnsubscriptionRequestBuilder() apiModel.PlcUnsubscriptionRequestBuilder {
+	panic("implement me")
+}
+
+func (m Connection) BrowseRequestBuilder() apiModel.PlcBrowseRequestBuilder {
+	panic("implement me")
+}
+
+func (m Connection) GetTransportInstance() transports.TransportInstance {
+	if mc, ok := m.messageCodec.(spi.TransportInstanceExposer); ok {
+		return mc.GetTransportInstance()
+	}
+	return nil
+}
+
+func (m Connection) GetPlcFieldHandler() spi.PlcFieldHandler {
+	return m.fieldHandler
+}
+
+func (m Connection) GetPlcValueHandler() spi.PlcValueHandler {
+	return m.valueHandler
+}
+
+func (m Connection) String() string {
+	return fmt.Sprintf("ads.Connection{}")
+}
diff --git a/plc4go/internal/plc4go/ads/Driver.go b/plc4go/internal/plc4go/ads/Driver.go
index a5f566b..b13bdb4 100644
--- a/plc4go/internal/plc4go/ads/Driver.go
+++ b/plc4go/internal/plc4go/ads/Driver.go
@@ -20,12 +20,16 @@ package ads
 
 import (
 	"github.com/apache/plc4x/plc4go/internal/plc4go/spi"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/transports"
 	"github.com/apache/plc4x/plc4go/pkg/plc4go"
+	"github.com/apache/plc4x/plc4go/pkg/plc4go/model"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+	"net/url"
 )
 
 type Driver struct {
 	fieldHandler spi.PlcFieldHandler
-	plc4go.PlcDriver
 }
 
 func NewDriver() plc4go.PlcDriver {
@@ -42,6 +46,49 @@ func (m Driver) GetProtocolName() string {
 	return "Beckhoff TwinCat ADS"
 }
 
+func (m Driver) GetDefaultTransport() string {
+	panic("implement me")
+}
+
+func (m Driver) CheckQuery(query string) error {
+	panic("implement me")
+}
+
+func (m Driver) GetConnection(transportUrl url.URL, transports map[string]transports.Transport, options map[string][]string) <-chan plc4go.PlcConnectionConnectResult {
+	log.Debug().Stringer("transportUrl", &transportUrl).Msgf("Get connection for transport url with %d transport(s) and %d option(s)", len(transports), len(options))
+	// Get an the transport specified in the url
+	transport, ok := transports[transportUrl.Scheme]
+	if !ok {
+		log.Error().Stringer("transportUrl", &transportUrl).Msgf("We couldn't find a transport for scheme %s", transportUrl.Scheme)
+		ch := make(chan plc4go.PlcConnectionConnectResult)
+		ch <- plc4go.NewPlcConnectionConnectResult(nil, errors.Errorf("couldn't find transport for given transport url %#v", transportUrl))
+		return ch
+	}
+	// Provide a default-port to the transport, which is used, if the user doesn't provide on in the connection string.
+	options["defaultTcpPort"] = []string{"48898"}
+	// Have the transport create a new transport-instance.
+	transportInstance, err := transport.CreateTransportInstance(transportUrl, options)
+	if err != nil {
+		log.Error().Stringer("transportUrl", &transportUrl).Msgf("We couldn't create a transport instance for port %#v", options["defaultTcpPort"])
+		ch := make(chan plc4go.PlcConnectionConnectResult)
+		ch <- plc4go.NewPlcConnectionConnectResult(nil, errors.New("couldn't initialize transport configuration for given transport url "+transportUrl.String()))
+		return ch
+	}
+
+	// Create a new codec for taking care of encoding/decoding of messages
+	codec := NewMessageCodec(transportInstance, nil)
+	log.Debug().Msgf("working with codec %#v", codec)
+
+	// Create the new connection
+	connection := NewConnection(codec, options, m.fieldHandler)
+	log.Info().Stringer("connection", connection).Msg("created connection, connecting now")
+	return connection.Connect()
+}
+
+func (m Driver) Discover(_ func(event model.PlcDiscoveryEvent)) error {
+	panic("not available")
+}
+
 func (m Driver) SupportsDiscovery() bool {
 	return false
 }
diff --git a/plc4go/internal/plc4go/ads/Field.go b/plc4go/internal/plc4go/ads/Field.go
new file mode 100644
index 0000000..21241ec
--- /dev/null
+++ b/plc4go/internal/plc4go/ads/Field.go
@@ -0,0 +1,113 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+package ads
+
+import (
+	"encoding/xml"
+	"fmt"
+	model2 "github.com/apache/plc4x/plc4go/internal/plc4go/ads/readwrite/model"
+	"github.com/apache/plc4x/plc4go/pkg/plc4go/model"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+)
+
+type PlcField struct {
+	FieldType        FieldType
+	IndexGroup       uint32
+	IndexOffset      uint32
+	SymbolicAddress  string
+	StringLength     int32
+	NumberOfElements int64
+	Datatype         model2.AdsDataType
+}
+
+func (m PlcField) GetAddressString() string {
+	return fmt.Sprintf("%dx%05d%05d%s%05d%05d:%s", m.FieldType, m.IndexGroup, m.IndexOffset, m.SymbolicAddress, m.StringLength, m.NumberOfElements, m.Datatype.String())
+}
+
+func (m PlcField) GetTypeName() string {
+	return m.FieldType.GetName()
+}
+
+func (m PlcField) GetQuantity() uint16 {
+	return 1
+}
+
+func NewAdsPlcField(fieldType FieldType, indexGroup uint32, indexOffset uint32, adsDataType model2.AdsDataType, stringLength int32, numberOfElements int64) (model.PlcField, error) {
+	return PlcField{
+		FieldType:        fieldType,
+		IndexGroup:       indexGroup,
+		IndexOffset:      indexOffset,
+		SymbolicAddress:  "",
+		StringLength:     stringLength,
+		NumberOfElements: numberOfElements,
+		Datatype:         adsDataType,
+	}, nil
+}
+
+func NewAdsSymbolicPlcField(fieldType FieldType, symbolicAddress string, adsDataType model2.AdsDataType, stringLength int32, numberOfElements int64) (model.PlcField, error) {
+	return PlcField{
+		FieldType:        fieldType,
+		IndexGroup:       0,
+		IndexOffset:      0,
+		SymbolicAddress:  symbolicAddress,
+		StringLength:     stringLength,
+		NumberOfElements: numberOfElements,
+		Datatype:         adsDataType,
+	}, nil
+}
+
+func CastToAdsFieldFromPlcField(plcField model.PlcField) (PlcField, error) {
+	if adsField, ok := plcField.(PlcField); ok {
+		return adsField, nil
+	}
+	return PlcField{}, errors.New("couldn't cast to AdsPlcField")
+}
+
+func (m PlcField) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+	log.Trace().Msg("MarshalXML")
+	if err := e.EncodeToken(xml.StartElement{Name: xml.Name{Local: m.FieldType.GetName()}}); err != nil {
+		return err
+	}
+
+	if err := e.EncodeElement(m.IndexGroup, xml.StartElement{Name: xml.Name{Local: "indexGroup"}}); err != nil {
+		return err
+	}
+	if err := e.EncodeElement(m.IndexOffset, xml.StartElement{Name: xml.Name{Local: "indexOffset"}}); err != nil {
+		return err
+	}
+	if err := e.EncodeElement(m.SymbolicAddress, xml.StartElement{Name: xml.Name{Local: "symbolicAddress"}}); err != nil {
+		return err
+	}
+	if err := e.EncodeElement(m.StringLength, xml.StartElement{Name: xml.Name{Local: "stringLength"}}); err != nil {
+		return err
+	}
+	if err := e.EncodeElement(m.NumberOfElements, xml.StartElement{Name: xml.Name{Local: "numberOfElements"}}); err != nil {
+		return err
+	}
+	if err := e.EncodeElement(m.Datatype.String(), xml.StartElement{Name: xml.Name{Local: "dataType"}}); err != nil {
+		return err
+	}
+
+	if err := e.EncodeToken(xml.EndElement{Name: xml.Name{Local: m.FieldType.GetName()}}); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/plc4go/internal/plc4go/ads/FieldHandler.go b/plc4go/internal/plc4go/ads/FieldHandler.go
index ce2a708..14e657e 100644
--- a/plc4go/internal/plc4go/ads/FieldHandler.go
+++ b/plc4go/internal/plc4go/ads/FieldHandler.go
@@ -19,19 +19,143 @@
 package ads
 
 import (
-	"errors"
-	"github.com/apache/plc4x/plc4go/internal/plc4go/spi"
+	"encoding/binary"
+	"encoding/hex"
+	"fmt"
+	model2 "github.com/apache/plc4x/plc4go/internal/plc4go/ads/readwrite/model"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/utils"
 	apiModel "github.com/apache/plc4x/plc4go/pkg/plc4go/model"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+	"regexp"
+	"strconv"
 )
 
+type FieldType uint8
+
+//go:generate stringer -type FieldType
+const (
+	StringField         FieldType = 0x00
+	Field               FieldType = 0x01
+	SymbolicStringField FieldType = 0x03
+	SymbolicField       FieldType = 0x04
+)
+
+func (i FieldType) GetName() string {
+	return fmt.Sprintf("AdsField%s", i.String())
+}
+
 type FieldHandler struct {
-	spi.PlcFieldHandler
+	directAdsStringField   *regexp.Regexp
+	directAdsField         *regexp.Regexp
+	symbolicAdsStringField *regexp.Regexp
+	symbolicAdsField       *regexp.Regexp
 }
 
 func NewFieldHandler() FieldHandler {
-	return FieldHandler{}
+	return FieldHandler{
+		directAdsStringField:   regexp.MustCompile("^((0[xX](?P<indexGroupHex>[0-9a-fA-F]+))|(?P<indexGroup>\\d+))/((0[xX](?P<indexOffsetHex>[0-9a-fA-F]+))|(?P<indexOffset>\\d+)):(?P<adsDataType>STRING|WSTRING)\\((?P<stringLength>\\d{1,3})\\)(\\[(?P<numberOfElements>\\d+)])?"),
+		directAdsField:         regexp.MustCompile("^((0[xX](?P<indexGroupHex>[0-9a-fA-F]+))|(?P<indexGroup>\\d+))/((0[xX](?P<indexOffsetHex>[0-9a-fA-F]+))|(?P<indexOffset>\\d+)):(?P<adsDataType>\\w+)(\\[(?P<numberOfElements>\\d+)])?"),
+		symbolicAdsStringField: regexp.MustCompile("^(?P<symbolicAddress>.+):(?P<adsDataType>'STRING'|'WSTRING')\\((?P<stringLength>\\d{1,3})\\)(\\[(?P<numberOfElements>\\d+)])?"),
+		symbolicAdsField:       regexp.MustCompile("^(?P<symbolicAddress>.+):(?P<adsDataType>\\w+)(\\[(?P<numberOfElements>\\d+)])?"),
+	}
 }
 
 func (m FieldHandler) ParseQuery(query string) (apiModel.PlcField, error) {
-	return nil, errors.New("Invalid address format for address '" + query + "'")
+	if match := utils.GetSubgroupMatches(m.directAdsStringField, query); match != nil {
+		var indexGroup uint32
+		if indexGroupHexString := match["indexGroupHex"]; indexGroupHexString != "" {
+			decodeString, err := hex.DecodeString(indexGroupHexString[2:])
+			if err != nil {
+				return nil, errors.Wrap(err, "Error decoding index group")
+			}
+			indexGroup = binary.BigEndian.Uint32(decodeString)
+		} else {
+			parsedIndexGroup, err := strconv.Atoi(match["indexGroup"])
+			if err != nil {
+				return nil, errors.Wrap(err, "Error decoding index group")
+			}
+			indexGroup = uint32(parsedIndexGroup)
+		}
+		var indexOffset uint32
+		if indexOffsetHexString := match["indexOffsetHex"]; indexOffsetHexString != "" {
+			decodeString, err := hex.DecodeString(indexOffsetHexString[2:])
+			if err != nil {
+				return nil, errors.Wrap(err, "Error decoding index group")
+			}
+			indexOffset = binary.BigEndian.Uint32(decodeString)
+		} else {
+			parsedIndexOffset, err := strconv.Atoi(match["indexOffset"])
+			if err != nil {
+				return nil, errors.Wrap(err, "Error decoding index group")
+			}
+			indexOffset = uint32(parsedIndexOffset)
+		}
+		stringLength, err := strconv.Atoi(match["stringLength"])
+		if err != nil {
+			return nil, errors.Wrap(err, "Error decoding string length")
+		}
+		numberOfElements, err := strconv.Atoi(match["numberOfElements"])
+		if err != nil {
+			log.Trace().Msg("Falling back to number of elements 1")
+			numberOfElements = 1
+		}
+
+		return NewAdsPlcField(StringField, indexGroup, indexOffset, model2.AdsDataTypeByName(match["datatype"]), int32(stringLength), int64(numberOfElements))
+	} else if match := utils.GetSubgroupMatches(m.directAdsField, query); match != nil {
+		var indexGroup uint32
+		if indexGroupHexString := match["indexGroupHex"]; indexGroupHexString != "" {
+			decodeString, err := hex.DecodeString(indexGroupHexString[2:])
+			if err != nil {
+				return nil, errors.Wrap(err, "Error decoding index group")
+			}
+			indexGroup = binary.BigEndian.Uint32(decodeString)
+		} else {
+			parsedIndexGroup, err := strconv.Atoi(match["indexGroup"])
+			if err != nil {
+				return nil, errors.Wrap(err, "Error decoding index group")
+			}
+			indexGroup = uint32(parsedIndexGroup)
+		}
+		var indexOffset uint32
+		if indexOffsetHexString := match["indexOffsetHex"]; indexOffsetHexString != "" {
+			decodeString, err := hex.DecodeString(indexOffsetHexString[2:])
+			if err != nil {
+				return nil, errors.Wrap(err, "Error decoding index group")
+			}
+			indexOffset = binary.BigEndian.Uint32(decodeString)
+		} else {
+			parsedIndexOffset, err := strconv.Atoi(match["indexOffset"])
+			if err != nil {
+				return nil, errors.Wrap(err, "Error decoding index group")
+			}
+			indexOffset = uint32(parsedIndexOffset)
+		}
+
+		adsDataType := model2.AdsDataTypeByName(match["datatype"])
+		numberOfElements, err := strconv.Atoi(match["numberOfElements"])
+		if err != nil {
+			log.Trace().Msg("Falling back to number of elements 1")
+			numberOfElements = 1
+		}
+		return NewAdsPlcField(Field, indexGroup, indexOffset, adsDataType, int32(0), int64(numberOfElements))
+	} else if match := utils.GetSubgroupMatches(m.symbolicAdsStringField, query); match != nil {
+		stringLength, err := strconv.Atoi(match["stringLength"])
+		if err != nil {
+			return nil, errors.Wrap(err, "Error decoding string length")
+		}
+		numberOfElements, err := strconv.Atoi(match["numberOfElements"])
+		if err != nil {
+			return nil, errors.Wrap(err, "Error decoding number of elements")
+		}
+		return NewAdsSymbolicPlcField(SymbolicStringField, match["symbolicAddress"], model2.AdsDataTypeByName(match["datatype"]), int32(stringLength), int64(numberOfElements))
+	} else if match := utils.GetSubgroupMatches(m.symbolicAdsStringField, query); match != nil {
+		numberOfElements, err := strconv.Atoi(match["numberOfElements"])
+		if err != nil {
+			return nil, errors.Wrap(err, "Error decoding number of elements")
+		}
+		return NewAdsSymbolicPlcField(SymbolicField, match["symbolicAddress"], model2.AdsDataTypeByName(match["datatype"]), int32(0), int64(numberOfElements))
+	} else {
+		return nil, errors.Errorf("Invalid address format for address '%s'", query)
+	}
 }
diff --git a/plc4go/internal/plc4go/ads/MessageCodec.go b/plc4go/internal/plc4go/ads/MessageCodec.go
new file mode 100644
index 0000000..6da826f
--- /dev/null
+++ b/plc4go/internal/plc4go/ads/MessageCodec.go
@@ -0,0 +1,217 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+package ads
+
+import (
+	"fmt"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/ads/readwrite/model"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/transports"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/utils"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+	"time"
+)
+
+type Expectation struct {
+	expiration     time.Time
+	acceptsMessage spi.AcceptsMessage
+	handleMessage  spi.HandleMessage
+	handleError    spi.HandleError
+}
+
+func (m Expectation) String() string {
+	return fmt.Sprintf("Expectation(expires at %v)", m.expiration)
+}
+
+type MessageCodec struct {
+	expectationCounter            int32
+	transportInstance             transports.TransportInstance
+	defaultIncomingMessageChannel chan interface{}
+	expectations                  []Expectation
+	running                       bool
+}
+
+func NewMessageCodec(transportInstance transports.TransportInstance, defaultIncomingMessageChannel chan interface{}) *MessageCodec {
+	codec := &MessageCodec{
+		expectationCounter:            1,
+		transportInstance:             transportInstance,
+		defaultIncomingMessageChannel: defaultIncomingMessageChannel,
+		expectations:                  []Expectation{},
+		running:                       true,
+	}
+	// TODO: should we better move this go func into Connect(). If not a better explanation why we start the worker so early
+	// Start a worker that handles processing of responses
+	go work(codec)
+	return codec
+}
+
+func (m *MessageCodec) Connect() error {
+	log.Info().Msg("Connecting")
+	err := m.transportInstance.Connect()
+	if err == nil {
+		m.running = true
+	}
+	return err
+}
+
+func (m *MessageCodec) Disconnect() error {
+	log.Info().Msg("Disconnecting")
+	m.running = false
+	return m.transportInstance.Close()
+}
+
+func (m *MessageCodec) Send(message interface{}) error {
+	log.Trace().Msg("Sending message")
+	// Cast the message to the correct type of struct
+	tcpPaket := model.CastAmsTCPPacket(message)
+	// Serialize the request
+	wb := utils.NewWriteBuffer()
+	err := tcpPaket.Serialize(*wb)
+	if err != nil {
+		return errors.Wrap(err, "error serializing request")
+	}
+
+	// Send it to the PLC
+	err = m.transportInstance.Write(wb.GetBytes())
+	if err != nil {
+		return errors.Wrap(err, "error sending request")
+	}
+	return nil
+}
+
+func (m *MessageCodec) Expect(acceptsMessage spi.AcceptsMessage, handleMessage spi.HandleMessage, handleError spi.HandleError, ttl time.Duration) error {
+	expectation := Expectation{
+		expiration:     time.Now().Add(ttl),
+		acceptsMessage: acceptsMessage,
+		handleMessage:  handleMessage,
+		handleError:    handleError,
+	}
+	m.expectations = append(m.expectations, expectation)
+	return nil
+}
+
+func (m *MessageCodec) SendRequest(message interface{}, acceptsMessage spi.AcceptsMessage, handleMessage spi.HandleMessage, handleError spi.HandleError, ttl time.Duration) error {
+	log.Trace().Msg("Sending request")
+	// Send the actual message
+	err := m.Send(message)
+	if err != nil {
+		return errors.Wrap(err, "Error sending the request")
+	}
+	return m.Expect(acceptsMessage, handleMessage, handleError, ttl)
+}
+
+func (m *MessageCodec) GetDefaultIncomingMessageChannel() chan interface{} {
+	return m.defaultIncomingMessageChannel
+}
+
+func (m *MessageCodec) receive() (interface{}, error) {
+	log.Trace().Msg("receiving")
+	// We need at least 6 bytes in order to know how big the packet is in total
+	if num, err := m.transportInstance.GetNumReadableBytes(); (err == nil) && (num >= 6) {
+		log.Debug().Msgf("we got %d readable bytes", num)
+		data, err := m.transportInstance.PeekReadableBytes(6)
+		if err != nil {
+			log.Warn().Err(err).Msg("error peeking")
+			// TODO: Possibly clean up ...
+			return nil, nil
+		}
+		// Get the size of the entire packet
+		// TODO: pretty sure this is wrong for ADS CP from modbus
+		packetSize := (uint32(data[4]) << 8) + uint32(data[5]) + 6
+		if num >= packetSize {
+			data, err = m.transportInstance.Read(packetSize)
+			if err != nil {
+				// TODO: Possibly clean up ...
+				return nil, nil
+			}
+			rb := utils.NewReadBuffer(data)
+			tcpPacket, err := model.AmsTCPPacketParse(rb)
+			if err != nil {
+				log.Warn().Err(err).Msg("error parsing")
+				// TODO: Possibly clean up ...
+				return nil, nil
+			}
+			return tcpPacket, nil
+		}
+	}
+	// TODO: maybe we return here a not enough error error
+	return nil, nil
+}
+
+func work(m *MessageCodec) {
+	// Start an endless loop
+	for m.running {
+		log.Trace().Msg("working")
+		if len(m.expectations) <= 0 {
+			// Sleep for 10ms
+			time.Sleep(time.Millisecond * 10)
+			continue
+		}
+		message, err := m.receive()
+		if err != nil {
+			log.Error().Err(err).Msg("got an error reading from transport")
+			continue
+		}
+		if message == nil {
+			time.Sleep(time.Millisecond * 10)
+			continue
+		}
+		now := time.Now()
+		messageHandled := false
+		// Go through all expectations
+		for index, expectation := range m.expectations {
+			// Check if this expectation has expired.
+			if now.After(expectation.expiration) {
+				log.Debug().Stringer("expectation", expectation).Msg("expired")
+				// Remove this expectation from the list.
+				m.expectations = append(m.expectations[:index], m.expectations[index+1:]...)
+				break
+			}
+
+			// Check if the current message matches the expectations
+			// If it does, let it handle the message.
+			if accepts := expectation.acceptsMessage(message); accepts {
+				log.Debug().Stringer("expectation", expectation).Msg("accepts message")
+				err = expectation.handleMessage(message)
+				if err == nil {
+					messageHandled = true
+					// Remove this expectation from the list.
+					m.expectations = append(m.expectations[:index], m.expectations[index+1:]...)
+				} else {
+					log.Error().Err(err).Msg("Error handling message")
+				}
+				break
+			}
+		}
+
+		// If the message has not been handled and a default handler is provided, call this ...
+		if !messageHandled {
+			if m.defaultIncomingMessageChannel != nil {
+				m.defaultIncomingMessageChannel <- message
+			} else {
+				log.Warn().Msgf("No handler registered for handling message %s", message)
+			}
+		}
+	}
+}
+
+func (m MessageCodec) GetTransportInstance() transports.TransportInstance {
+	return m.transportInstance
+}
diff --git a/plc4go/internal/plc4go/ads/Reader.go b/plc4go/internal/plc4go/ads/Reader.go
new file mode 100644
index 0000000..904a933
--- /dev/null
+++ b/plc4go/internal/plc4go/ads/Reader.go
@@ -0,0 +1,217 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+package ads
+
+import (
+	readWriteModel "github.com/apache/plc4x/plc4go/internal/plc4go/ads/readwrite/model"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi"
+	plc4goModel "github.com/apache/plc4x/plc4go/internal/plc4go/spi/model"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/utils"
+	"github.com/apache/plc4x/plc4go/pkg/plc4go/model"
+	"github.com/apache/plc4x/plc4go/pkg/plc4go/values"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+	"math"
+	"sync/atomic"
+	"time"
+)
+
+type Reader struct {
+	transactionIdentifier uint32
+	messageCodec          spi.MessageCodec
+}
+
+func NewReader(messageCodec spi.MessageCodec) *Reader {
+	return &Reader{
+		transactionIdentifier: 0,
+		messageCodec:          messageCodec,
+	}
+}
+
+func (m *Reader) Read(readRequest model.PlcReadRequest) <-chan model.PlcReadRequestResult {
+	log.Trace().Msg("Reading")
+	result := make(chan model.PlcReadRequestResult)
+	go func() {
+		if len(readRequest.GetFieldNames()) != 1 {
+			result <- model.PlcReadRequestResult{
+				Request:  readRequest,
+				Response: nil,
+				Err:      errors.New("ads only supports single-item requests"),
+			}
+			log.Debug().Msgf("ads only supports single-item requests. Got %d fields", len(readRequest.GetFieldNames()))
+			return
+		}
+		// If we are requesting only one field, use a
+		fieldName := readRequest.GetFieldNames()[0]
+		field := readRequest.GetField(fieldName)
+		adsField, err := CastToAdsFieldFromPlcField(field)
+		if err != nil {
+			result <- model.PlcReadRequestResult{
+				Request:  readRequest,
+				Response: nil,
+				Err:      errors.Wrap(err, "invalid field item type"),
+			}
+			log.Debug().Msgf("Invalid field item type %T", field)
+			return
+		}
+		// TODO: move TargetAmsNetId, TargetAmsPort, SourceAmsNetId, SourceAmsPort to Connection
+		userdata := readWriteModel.AmsPacket{
+			TargetAmsNetId: &readWriteModel.AmsNetId{
+				Octet1: 0,
+				Octet2: 0,
+				Octet3: 0,
+				Octet4: 0,
+				Octet5: 0,
+				Octet6: 0,
+			},
+			TargetAmsPort: 0,
+			SourceAmsNetId: &readWriteModel.AmsNetId{
+				Octet1: 0,
+				Octet2: 0,
+				Octet3: 0,
+				Octet4: 0,
+				Octet5: 0,
+				Octet6: 0,
+			},
+			SourceAmsPort: 0,
+			CommandId:     readWriteModel.CommandId_ADS_READ,
+			State:         readWriteModel.NewState(false, false, false, false, false, true, false, false, false),
+			ErrorCode:     0,
+			InvokeId:      0,
+			Data:          nil,
+		}
+		switch adsField.FieldType {
+		case StringField:
+			// TODO: what is our read length?
+			userdata.Data = readWriteModel.NewAdsReadRequest(adsField.IndexGroup, adsField.IndexOffset, 1)
+		case Field:
+			// TODO: what is our read length?
+			userdata.Data = readWriteModel.NewAdsReadRequest(adsField.IndexGroup, adsField.IndexOffset, 1)
+		case SymbolicStringField:
+			panic("implement me")
+		case SymbolicField:
+			panic("implement me")
+		default:
+			result <- model.PlcReadRequestResult{
+				Request:  readRequest,
+				Response: nil,
+				Err:      errors.Errorf("unsupported field type %x", adsField.FieldType),
+			}
+			log.Debug().Msgf("Unsupported field type %x", adsField.FieldType)
+			return
+		}
+
+		// Calculate a new transaction identifier
+		transactionIdentifier := atomic.AddUint32(&m.transactionIdentifier, 1)
+		if transactionIdentifier > math.MaxUint8 {
+			transactionIdentifier = 1
+			atomic.StoreUint32(&m.transactionIdentifier, 1)
+		}
+		log.Debug().Msgf("Calculated transaction identifier %x", transactionIdentifier)
+		userdata.InvokeId = transactionIdentifier
+
+		// Assemble the finished tcp paket
+		log.Trace().Msg("Assemble tcp paket")
+		amsTcpPaket := readWriteModel.AmsTCPPacket{
+			Userdata: &userdata,
+		}
+
+		// Send the TCP Paket over the wire
+		log.Trace().Msg("Send TCP Paket")
+		if err = m.messageCodec.SendRequest(
+			amsTcpPaket,
+			func(message interface{}) bool {
+				paket := readWriteModel.CastAmsTCPPacket(message)
+				return paket.Userdata.InvokeId == transactionIdentifier
+			},
+			func(message interface{}) error {
+				// Convert the response into an ADU
+				log.Trace().Msg("convert response to ADU")
+				amsTcpPaket := readWriteModel.CastAmsTCPPacket(message)
+				// Convert the ads response into a PLC4X response
+				log.Trace().Msg("convert response to PLC4X response")
+				readResponse, err := m.ToPlc4xReadResponse(*amsTcpPaket, readRequest)
+
+				if err != nil {
+					result <- model.PlcReadRequestResult{
+						Request: readRequest,
+						Err:     errors.Wrap(err, "Error decoding response"),
+					}
+					// TODO: should we return the error here?
+					return nil
+				}
+				result <- model.PlcReadRequestResult{
+					Request:  readRequest,
+					Response: readResponse,
+				}
+				return nil
+			},
+			func(err error) error {
+				result <- model.PlcReadRequestResult{
+					Request: readRequest,
+					Err:     errors.Wrap(err, "got timeout while waiting for response"),
+				}
+				return nil
+			},
+			time.Second*1); err != nil {
+			result <- model.PlcReadRequestResult{
+				Request:  readRequest,
+				Response: nil,
+				Err:      errors.Wrap(err, "error sending message"),
+			}
+		}
+	}()
+	return result
+}
+
+func (m *Reader) ToPlc4xReadResponse(amsTcpPaket readWriteModel.AmsTCPPacket, readRequest model.PlcReadRequest) (model.PlcReadResponse, error) {
+	var data []uint8
+	switch amsTcpPaket.Userdata.Data.Child.(type) {
+	case *readWriteModel.AdsReadResponse:
+		readResponse := readWriteModel.CastAdsReadResponse(amsTcpPaket.Userdata.Data)
+		data = utils.Int8ArrayToUint8Array(readResponse.Data)
+		// Pure Boolean ...
+	default:
+		return nil, errors.Errorf("unsupported response type %T", amsTcpPaket.Userdata.Data.Child)
+	}
+
+	// Get the field from the request
+	log.Trace().Msg("get a field from request")
+	fieldName := readRequest.GetFieldNames()[0]
+	field, err := CastToAdsFieldFromPlcField(readRequest.GetField(fieldName))
+	if err != nil {
+		return nil, errors.Wrap(err, "error casting to ads-field")
+	}
+
+	// Decode the data according to the information from the request
+	log.Trace().Msg("decode data")
+	rb := utils.NewReadBuffer(data)
+	value, err := readWriteModel.DataItemParse(rb, field.Datatype.DataFormatName(), field.StringLength)
+	if err != nil {
+		return nil, errors.Wrap(err, "Error parsing data item")
+	}
+	responseCodes := map[string]model.PlcResponseCode{}
+	plcValues := map[string]values.PlcValue{}
+	plcValues[fieldName] = value
+	responseCodes[fieldName] = model.PlcResponseCode_OK
+
+	// Return the response
+	log.Trace().Msg("Returning the response")
+	return plc4goModel.NewDefaultPlcReadResponse(readRequest, responseCodes, plcValues), nil
+}
diff --git a/plc4go/internal/plc4go/ads/FieldHandler.go b/plc4go/internal/plc4go/ads/ValueHandler.go
similarity index 68%
copy from plc4go/internal/plc4go/ads/FieldHandler.go
copy to plc4go/internal/plc4go/ads/ValueHandler.go
index ce2a708..5290c3e 100644
--- a/plc4go/internal/plc4go/ads/FieldHandler.go
+++ b/plc4go/internal/plc4go/ads/ValueHandler.go
@@ -19,19 +19,13 @@
 package ads
 
 import (
-	"errors"
-	"github.com/apache/plc4x/plc4go/internal/plc4go/spi"
-	apiModel "github.com/apache/plc4x/plc4go/pkg/plc4go/model"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/values"
 )
 
-type FieldHandler struct {
-	spi.PlcFieldHandler
+type ValueHandler struct {
+	values.IEC61131ValueHandler
 }
 
-func NewFieldHandler() FieldHandler {
-	return FieldHandler{}
-}
-
-func (m FieldHandler) ParseQuery(query string) (apiModel.PlcField, error) {
-	return nil, errors.New("Invalid address format for address '" + query + "'")
+func NewValueHandler() ValueHandler {
+	return ValueHandler{}
 }
diff --git a/plc4go/internal/plc4go/ads/Writer.go b/plc4go/internal/plc4go/ads/Writer.go
new file mode 100644
index 0000000..1228370
--- /dev/null
+++ b/plc4go/internal/plc4go/ads/Writer.go
@@ -0,0 +1,200 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+package ads
+
+import (
+	readWriteModel "github.com/apache/plc4x/plc4go/internal/plc4go/ads/readwrite/model"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi"
+	plc4goModel "github.com/apache/plc4x/plc4go/internal/plc4go/spi/model"
+	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/utils"
+	"github.com/apache/plc4x/plc4go/pkg/plc4go/model"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+	"math"
+	"sync/atomic"
+	"time"
+)
+
+type Writer struct {
+	transactionIdentifier uint32
+	messageCodec          spi.MessageCodec
+}
+
+func NewWriter(messageCodec spi.MessageCodec) Writer {
+	return Writer{
+		transactionIdentifier: 0,
+		messageCodec:          messageCodec,
+	}
+}
+
+func (m Writer) Write(writeRequest model.PlcWriteRequest) <-chan model.PlcWriteRequestResult {
+	result := make(chan model.PlcWriteRequestResult)
+	go func() {
+		// If we are requesting only one field, use a
+		if len(writeRequest.GetFieldNames()) != 1 {
+			result <- model.PlcWriteRequestResult{
+				Request:  writeRequest,
+				Response: nil,
+				Err:      errors.New("ads only supports single-item requests"),
+			}
+			return
+		}
+		fieldName := writeRequest.GetFieldNames()[0]
+
+		// Get the ads field instance from the request
+		field := writeRequest.GetField(fieldName)
+		adsField, err := CastToAdsFieldFromPlcField(field)
+		if err != nil {
+			result <- model.PlcWriteRequestResult{
+				Request:  writeRequest,
+				Response: nil,
+				Err:      errors.Wrap(err, "invalid field item type"),
+			}
+			return
+		}
+
+		// Get the value from the request and serialize it to a byte array
+		value := writeRequest.GetValue(fieldName)
+		io := utils.NewWriteBuffer()
+		if err := readWriteModel.DataItemSerialize(io, value, adsField.Datatype.DataFormatName(), adsField.StringLength); err != nil {
+			result <- model.PlcWriteRequestResult{
+				Request:  writeRequest,
+				Response: nil,
+				Err:      errors.Wrap(err, "error serializing value"),
+			}
+			return
+		}
+		data := utils.Uint8ArrayToInt8Array(io.GetBytes())
+
+		// TODO: move TargetAmsNetId, TargetAmsPort, SourceAmsNetId, SourceAmsPort to Connection
+		userdata := readWriteModel.AmsPacket{
+			TargetAmsNetId: &readWriteModel.AmsNetId{
+				Octet1: 0,
+				Octet2: 0,
+				Octet3: 0,
+				Octet4: 0,
+				Octet5: 0,
+				Octet6: 0,
+			},
+			TargetAmsPort: 0,
+			SourceAmsNetId: &readWriteModel.AmsNetId{
+				Octet1: 0,
+				Octet2: 0,
+				Octet3: 0,
+				Octet4: 0,
+				Octet5: 0,
+				Octet6: 0,
+			},
+			SourceAmsPort: 0,
+			CommandId:     readWriteModel.CommandId_ADS_READ,
+			State:         readWriteModel.NewState(false, false, false, false, false, true, false, false, false),
+			ErrorCode:     0,
+			InvokeId:      0,
+			Data:          nil,
+		}
+		switch adsField.FieldType {
+		case StringField:
+			// TODO: what is our read length?
+			userdata.Data = readWriteModel.NewAdsWriteRequest(adsField.IndexGroup, adsField.IndexOffset, data)
+			panic("implement me")
+		case Field:
+			panic("implement me")
+		case SymbolicStringField:
+			panic("implement me")
+		case SymbolicField:
+			panic("implement me")
+		default:
+			result <- model.PlcWriteRequestResult{
+				Request:  writeRequest,
+				Response: nil,
+				Err:      errors.New("unsupported field type"),
+			}
+			return
+		}
+
+		// Calculate a new unit identifier
+		transactionIdentifier := atomic.AddUint32(&m.transactionIdentifier, 1)
+		if transactionIdentifier > math.MaxUint8 {
+			transactionIdentifier = 0
+			atomic.StoreUint32(&m.transactionIdentifier, 0)
+		}
+		userdata.InvokeId = transactionIdentifier
+
+		// Assemble the finished ADU
+		log.Trace().Msg("Assemble ADU")
+		amsTcpPaket := readWriteModel.AmsTCPPacket{
+			Userdata: &userdata,
+		}
+
+		// Send the TCP Paket over the wire
+		err = m.messageCodec.SendRequest(
+			amsTcpPaket,
+			func(message interface{}) bool {
+				paket := readWriteModel.CastAmsTCPPacket(message)
+				return paket.Userdata.InvokeId == transactionIdentifier
+			},
+			func(message interface{}) error {
+				// Convert the response into an ADU
+				responseAmsTcpPaket := readWriteModel.CastAmsTCPPacket(message)
+				// Convert the ads response into a PLC4X response
+				readResponse, err := m.ToPlc4xWriteResponse(amsTcpPaket, *responseAmsTcpPaket, writeRequest)
+
+				if err != nil {
+					result <- model.PlcWriteRequestResult{
+						Request: writeRequest,
+						Err:     errors.Wrap(err, "Error decoding response"),
+					}
+				} else {
+					result <- model.PlcWriteRequestResult{
+						Request:  writeRequest,
+						Response: readResponse,
+					}
+				}
+				return nil
+			},
+			func(err error) error {
+				result <- model.PlcWriteRequestResult{
+					Request: writeRequest,
+					Err:     errors.New("got timeout while waiting for response"),
+				}
+				return nil
+			},
+			time.Second*1)
+	}()
+	return result
+}
+
+func (m Writer) ToPlc4xWriteResponse(requestTcpPaket readWriteModel.AmsTCPPacket, responseTcpPaket readWriteModel.AmsTCPPacket, writeRequest model.PlcWriteRequest) (model.PlcWriteResponse, error) {
+	responseCodes := map[string]model.PlcResponseCode{}
+	fieldName := writeRequest.GetFieldNames()[0]
+
+	// we default to an error until its proven wrong
+	responseCodes[fieldName] = model.PlcResponseCode_INTERNAL_ERROR
+	switch responseTcpPaket.Userdata.Data.Child.(type) {
+	case *readWriteModel.AdsWriteResponse:
+		resp := readWriteModel.CastAdsWriteResponse(responseTcpPaket.Userdata.Data)
+		responseCodes[fieldName] = model.PlcResponseCode(resp.Result)
+	default:
+		return nil, errors.Errorf("unsupported response type %T", responseTcpPaket.Userdata.Data.Child)
+	}
+
+	// Return the response
+	log.Trace().Msg("Returning the response")
+	return plc4goModel.NewDefaultPlcWriteResponse(writeRequest, responseCodes), nil
+}
diff --git a/plc4go/internal/plc4go/ads/fieldtype_string.go b/plc4go/internal/plc4go/ads/fieldtype_string.go
new file mode 100644
index 0000000..86d1910
--- /dev/null
+++ b/plc4go/internal/plc4go/ads/fieldtype_string.go
@@ -0,0 +1,54 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Code generated by "stringer -type FieldType"; DO NOT EDIT.
+
+package ads
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[StringField-0]
+	_ = x[Field-1]
+	_ = x[SymbolicStringField-3]
+	_ = x[SymbolicField-4]
+}
+
+const (
+	_FieldType_name_0 = "StringFieldField"
+	_FieldType_name_1 = "SymbolicStringFieldSymbolicField"
+)
+
+var (
+	_FieldType_index_0 = [...]uint8{0, 11, 16}
+	_FieldType_index_1 = [...]uint8{0, 19, 32}
+)
+
+func (i FieldType) String() string {
+	switch {
+	case i <= 1:
+		return _FieldType_name_0[_FieldType_index_0[i]:_FieldType_index_0[i+1]]
+	case 3 <= i && i <= 4:
+		i -= 3
+		return _FieldType_name_1[_FieldType_index_1[i]:_FieldType_index_1[i+1]]
+	default:
+		return "FieldType(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+}
diff --git a/plc4go/internal/plc4go/modbus/FieldHandler.go b/plc4go/internal/plc4go/modbus/FieldHandler.go
index 0a7b951..abadb1f 100644
--- a/plc4go/internal/plc4go/modbus/FieldHandler.go
+++ b/plc4go/internal/plc4go/modbus/FieldHandler.go
@@ -19,11 +19,11 @@
 package modbus
 
 import (
-	"errors"
 	"fmt"
 	model2 "github.com/apache/plc4x/plc4go/internal/plc4go/modbus/readwrite/model"
 	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/utils"
 	"github.com/apache/plc4x/plc4go/pkg/plc4go/model"
+	"github.com/pkg/errors"
 	"regexp"
 )
 
@@ -94,5 +94,5 @@ func (m FieldHandler) ParseQuery(query string) (model.PlcField, error) {
 	} else if match := utils.GetSubgroupMatches(m.numericExtendedRegisterPattern, query); match != nil {
 		return NewModbusPlcFieldFromStrings(ExtendedRegister, match["address"], match["quantity"], model2.ModbusDataTypeByName(match["datatype"]))
 	}
-	return nil, errors.New("Invalid address format for address '" + query + "'")
+	return nil, errors.Errorf("Invalid address format for address '%s'", query)
 }
diff --git a/plc4go/internal/plc4go/spi/testutils/DriverTestRunner.go b/plc4go/internal/plc4go/spi/testutils/DriverTestRunner.go
index d304ac1..e4ff029 100644
--- a/plc4go/internal/plc4go/spi/testutils/DriverTestRunner.go
+++ b/plc4go/internal/plc4go/spi/testutils/DriverTestRunner.go
@@ -22,7 +22,8 @@ import (
 	"encoding/hex"
 	"encoding/xml"
 	"fmt"
-	model2 "github.com/apache/plc4x/plc4go/internal/plc4go/modbus/readwrite"
+	adsModel "github.com/apache/plc4x/plc4go/internal/plc4go/ads/readwrite"
+	modbusModel "github.com/apache/plc4x/plc4go/internal/plc4go/modbus/readwrite"
 	"github.com/apache/plc4x/plc4go/internal/plc4go/spi"
 	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/transports"
 	"github.com/apache/plc4x/plc4go/internal/plc4go/spi/transports/test"
@@ -33,6 +34,7 @@ import (
 	"github.com/rs/zerolog/log"
 	"github.com/subchen/go-xmldom"
 	"os"
+	"runtime/debug"
 	"strconv"
 	"testing"
 	"time"
@@ -135,7 +137,7 @@ func (m DriverTestsuite) ExecuteStep(connection plc4go.PlcConnection, testcase *
 				}
 				field, err := he.GetPlcFieldHandler().ParseQuery(fieldAddress)
 				if err != nil {
-					return errors.Wrap(err, "error parsing address: "+fieldAddress)
+					return errors.Wrapf(err, "error parsing address: %s", fieldAddress)
 				}
 				if field.GetQuantity() > 1 {
 					var fieldValue []string
@@ -203,9 +205,21 @@ func (m DriverTestsuite) ExecuteStep(connection plc4go.PlcConnection, testcase *
 
 		// Parse the xml into a real model
 		log.Trace().Msg("parsing xml")
-		message, err := model2.ModbusXmlParserHelper{}.Parse(typeName, payloadString)
-		if err != nil {
-			return errors.Wrap(err, "error parsing xml")
+		var message interface{}
+		var err error
+		switch m.driverName {
+		case "modbus":
+			message, err = modbusModel.ModbusXmlParserHelper{}.Parse(typeName, payloadString)
+			if err != nil {
+				return errors.Wrap(err, "error parsing xml")
+			}
+		case "ads":
+			message, err = adsModel.AdsXmlParserHelper{}.Parse(typeName, payloadString)
+			if err != nil {
+				return errors.Wrap(err, "error parsing xml")
+			}
+		default:
+			return errors.Errorf("Driver name %s has not mapped parser", m.driverName)
 		}
 
 		// Serialize the model into bytes
@@ -270,9 +284,21 @@ func (m DriverTestsuite) ExecuteStep(connection plc4go.PlcConnection, testcase *
 
 		// Parse the xml into a real model
 		log.Trace().Msg("Parsing model")
-		message, err := model2.ModbusXmlParserHelper{}.Parse(typeName, payloadString)
-		if err != nil {
-			return errors.Wrap(err, "error parsing xml")
+		var message interface{}
+		var err error
+		switch m.driverName {
+		case "modbus":
+			message, err = modbusModel.ModbusXmlParserHelper{}.Parse(typeName, payloadString)
+			if err != nil {
+				return errors.Wrap(err, "error parsing xml")
+			}
+		case "ads":
+			message, err = adsModel.AdsXmlParserHelper{}.Parse(typeName, payloadString)
+			if err != nil {
+				return errors.Wrap(err, "error parsing xml")
+			}
+		default:
+			return errors.Errorf("Driver name %s has not mapped parser", m.driverName)
 		}
 
 		// Serialize the model into bytes
@@ -395,6 +421,12 @@ func RunDriverTestsuite(t *testing.T, driver plc4go.PlcDriver, testPath string,
 
 	for _, testcase := range testsuite.testcases {
 		t.Run(testcase.name, func(t *testing.T) {
+			defer func() {
+				if err := recover(); err != nil {
+					log.Error().Msgf("\n\n-------------------------------------------------------\nFatal Failure\n%+v\n%s\n-------------------------------------------------------\n", err, debug.Stack())
+					t.FailNow()
+				}
+			}()
 			if skippedTestCasesMap[testcase.name] {
 				log.Warn().Msgf("Testcase %s skipped", testcase.name)
 				t.Skipf("Testcase %s skipped", testcase.name)
@@ -403,7 +435,7 @@ func RunDriverTestsuite(t *testing.T, driver plc4go.PlcDriver, testPath string,
 			log.Info().Msgf("Running testcase %s", testcase.name)
 			err := testsuite.Run(driverManager, testcase)
 			if err != nil {
-				log.Err(err).Msgf("\n\n-------------------------------------------------------\nFailure\n%s\n-------------------------------------------------------\n", err.Error())
+				log.Error().Err(err).Msgf("\n\n-------------------------------------------------------\nFailure\n%+v\n-------------------------------------------------------\n", err)
 				t.Fail()
 			}
 		})
diff --git a/plc4go/pkg/plc4go/driverManager.go b/plc4go/pkg/plc4go/driverManager.go
index 2ca314e..45e90ad 100644
--- a/plc4go/pkg/plc4go/driverManager.go
+++ b/plc4go/pkg/plc4go/driverManager.go
@@ -121,13 +121,12 @@ func (m PlcDriverManger) ListTransportNames() []string {
 	return transportNames
 }
 
-func (m PlcDriverManger) GetTransport(transportName string, connectionString string, options map[string][]string) (transports.Transport, error) {
-	// TODO: what are the parameters above for? If not needed we can blank ("_") them
+func (m PlcDriverManger) GetTransport(transportName string, _ string, _ map[string][]string) (transports.Transport, error) {
 	if val, ok := m.transports[transportName]; ok {
-		log.Debug().Str("transportName", transportName).Msg("Returning transport name")
+		log.Debug().Str("transportName", transportName).Msg("Returning transport")
 		return val, nil
 	}
-	return nil, errors.New("couldn't find transport " + transportName)
+	return nil, errors.Errorf("couldn't find transport %s", transportName)
 }
 
 func (m PlcDriverManger) GetConnection(connectionString string) <-chan PlcConnectionConnectResult {
@@ -213,8 +212,7 @@ func (m PlcDriverManger) Discover(callback func(event model.PlcDiscoveryEvent))
 		if driver.SupportsDiscovery() {
 			err := driver.Discover(callback)
 			if err != nil {
-				return errors.New("Error running Discover on driver " + driver.GetProtocolName() +
-					". Got error: " + err.Error())
+				return errors.Wrapf(err, "Error running Discover on driver %s", driver.GetProtocolName())
 			}
 		}
 	}
diff --git a/plc4go/pom.xml b/plc4go/pom.xml
index c21fe7a..13f2b7e 100644
--- a/plc4go/pom.xml
+++ b/plc4go/pom.xml
@@ -392,6 +392,22 @@
                 <flag>Apache Software Foundation (ASF)</flag>
               </buildFlags>
               <packages>
+                <package>internal/plc4go/ads/fieldtype_string.go</package>
+              </packages>
+            </configuration>
+          </execution>
+          <execution>
+            <id>add-license3</id>
+            <goals>
+              <goal>custom</goal>
+            </goals>
+            <configuration>
+              <exec>go-licenser</exec>
+              <customCommand>-licensor</customCommand>
+              <buildFlags>
+                <flag>Apache Software Foundation (ASF)</flag>
+              </buildFlags>
+              <packages>
                 <package>internal/plc4go/spi/testutils/steptype_string.go</package>
               </packages>
             </configuration>