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 2019/12/11 10:32:37 UTC

[plc4x] branch feature/ams_ads_mpsec updated: some progress on sandbox ads implementation

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

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


The following commit(s) were added to refs/heads/feature/ams_ads_mpsec by this push:
     new cbfae24  some progress on sandbox ads implementation
cbfae24 is described below

commit cbfae24e31640715fe561b5fb9ac18b7b179224f
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Wed Dec 11 11:32:24 2019 +0100

    some progress on sandbox ads implementation
---
 sandbox/test-java-amsads-driver/pom.xml            |    5 +
 .../apache/plc4x/java/amsads/AMSADSPlcDriver.java  |   73 +-
 .../org/apache/plc4x/java/amsads/Df1Field.java     |   62 --
 .../connection/AdsAbstractPlcConnection.java       |  240 +++++
 .../amsads/connection/AdsConnectionFactory.java    |   57 ++
 .../amsads/connection/AdsSerialPlcConnection.java  |   68 ++
 .../amsads/connection/AdsTcpPlcConnection.java     |  369 +++++++
 .../amsads/connection/SerialAMSADSConnection.java  |  148 ---
 .../plc4x/java/amsads/model/AdsDataType.java       |  577 +++++++++++
 .../{fields/DataType.java => model/AdsField.java}  |   30 +-
 .../java/amsads/model/AdsPlcFieldHandler.java      | 1015 ++++++++++++++++++++
 .../java/amsads/model/AdsSubscriptionHandle.java   |   85 ++
 .../plc4x/java/amsads/model/DirectAdsField.java    |  141 +++
 .../plc4x/java/amsads/model/SymbolicAdsField.java  |  105 ++
 .../package-info.java}                             |   22 +-
 .../plc4x/java/amsads/protocol/AMSADSProtocol.java |  157 ---
 .../java/amsads/protocol/Ads2PayloadProtocol.java  |  387 ++++++++
 .../amsads/protocol/Payload2SerialProtocol.java    |  171 ++++
 .../java/amsads/protocol/Payload2TcpProtocol.java  |   67 ++
 .../java/amsads/protocol/Plc4XDf1Protocol.java     |  172 ----
 .../java/amsads/protocol/Plc4x2AdsProtocol.java    |  445 +++++++++
 .../amsads/protocol/exception/AdsException.java    |   56 ++
 .../exception/AdsProtocolOverflowException.java}   |   23 +-
 .../java/amsads/protocol/util/DigestUtil.java      |   59 ++
 .../amsads/protocol/util/LittleEndianDecoder.java  |  357 +++++++
 .../amsads/protocol/util/LittleEndianEncoder.java  |  285 ++++++
 .../protocol/util/SingleMessageRateLimiter.java    |  124 +++
 .../util/package-info.java}                        |   32 +-
 .../apache/plc4x/java/amsads/util/DF1Utils.java    |  158 ---
 29 files changed, 4697 insertions(+), 793 deletions(-)

diff --git a/sandbox/test-java-amsads-driver/pom.xml b/sandbox/test-java-amsads-driver/pom.xml
index f9efcf5..62e732d 100644
--- a/sandbox/test-java-amsads-driver/pom.xml
+++ b/sandbox/test-java-amsads-driver/pom.xml
@@ -83,6 +83,11 @@
     </dependency>
 
     <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-configuration2</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>io.netty</groupId>
       <artifactId>netty-buffer</artifactId>
     </dependency>
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/AMSADSPlcDriver.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/AMSADSPlcDriver.java
index 34b584f..b8d0d3a 100644
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/AMSADSPlcDriver.java
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/AMSADSPlcDriver.java
@@ -18,57 +18,104 @@
  */
 package org.apache.plc4x.java.amsads;
 
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.plc4x.java.amsads.connection.AdsConnectionFactory;
+import org.apache.plc4x.java.amsads.readwrite.AmsNetId;
 import org.apache.plc4x.java.api.PlcConnection;
 import org.apache.plc4x.java.api.authentication.PlcAuthentication;
 import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
-import org.apache.plc4x.java.df1.connection.SerialDf1Connection;
 import org.apache.plc4x.java.spi.PlcDriver;
 
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
+/**
+ * Implementation of the ADS protocol, based on:
+ * - ADS Protocol
+ * - TCP
+ * - Serial
+ */
 public class AMSADSPlcDriver implements PlcDriver {
 
-    // TODO there is only serial, I guess?
+    public static final Pattern AMS_NET_ID_PATTERN =
+        Pattern.compile("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}");
+
+    public static final Pattern AMS_PORT_PATTERN = Pattern.compile("\\d+");
+
+    public static final Pattern ADS_ADDRESS_PATTERN =
+        Pattern.compile("(?<targetAmsNetId>" + AMS_NET_ID_PATTERN + "):(?<targetAmsPort>" + AMS_PORT_PATTERN + ")"
+            + "(/"
+            + "(?<sourceAmsNetId>" + AMS_NET_ID_PATTERN + "):(?<sourceAmsPort>" + AMS_PORT_PATTERN + ")"
+            + ")?");
     public static final Pattern INET_ADDRESS_PATTERN = Pattern.compile("tcp://(?<host>[\\w.]+)(:(?<port>\\d*))?");
-    public static final Pattern SERIAL_PATTERN = Pattern.compile("serial://(?<serialDefinition>/?[a-zA-Z0-9/]*)");
-    public static final Pattern DF1_URI_PATTERN = Pattern.compile("^df1:(" + INET_ADDRESS_PATTERN + "|" + SERIAL_PATTERN + ")/?" + "(?<params>\\?.*)?");
+    public static final Pattern SERIAL_PATTERN = Pattern.compile("serial://(?<serialDefinition>((?!/\\d).)*)");
+    public static final Pattern ADS_URI_PATTERN = Pattern.compile("^ads:(" + INET_ADDRESS_PATTERN + "|" + SERIAL_PATTERN + ")/" + ADS_ADDRESS_PATTERN + "(\\?.*)?");
+
+    private AdsConnectionFactory adsConnectionFactory;
+
+    public AMSADSPlcDriver() {
+        this.adsConnectionFactory = new AdsConnectionFactory();
+    }
+
+    public AMSADSPlcDriver(AdsConnectionFactory adsConnectionFactory) {
+        this.adsConnectionFactory = adsConnectionFactory;
+    }
 
     @Override
     public String getProtocolCode() {
-        return "df1";
+        return "ads";
     }
 
     @Override
     public String getProtocolName() {
-        return "Allen-Bradley DF1";
+        return "Beckhoff Twincat ADS";
     }
 
     @Override
     public PlcConnection connect(String url) throws PlcConnectionException {
-        Matcher matcher = DF1_URI_PATTERN.matcher(url);
+        Matcher matcher = ADS_URI_PATTERN.matcher(url);
         if (!matcher.matches()) {
             throw new PlcConnectionException(
-                "Connection url doesn't match the format 'df1:{type}//{port|host}'");
+                "Connection url " + url + " doesn't match 'ads://{{host|ip}|serial:definition}/{targetAmsNetId}:{targetAmsPort}/{sourceAmsNetId}:{sourceAmsPort}' RAW:" + ADS_URI_PATTERN);
         }
-
         String host = matcher.group("host");
         String serialDefinition = matcher.group("serialDefinition");
         String portString = matcher.group("port");
         Integer port = StringUtils.isNotBlank(portString) ? Integer.parseInt(portString) : null;
-        String params = matcher.group("params") != null ? matcher.group("params").substring(1) : null;
+        AmsNetId targetAmsNetId = AmsNetIdOf(matcher.group("targetAmsNetId"));
+        int targetAmsPort = Integer.parseInt(matcher.group("targetAmsPort"));
+        String sourceAmsNetIdString = matcher.group("sourceAmsNetId");
+        AmsNetId sourceAmsNetId = StringUtils.isNotBlank(sourceAmsNetIdString) ? AmsNetIdOf(sourceAmsNetIdString) : null;
+        String sourceAmsPortString = matcher.group("sourceAmsPort");
+        int sourceAmsPort = StringUtils.isNotBlank(sourceAmsPortString) ? Integer.parseInt(sourceAmsPortString) : null;
 
         if (serialDefinition != null) {
-            return new SerialDf1Connection(serialDefinition, params);
+            return adsConnectionFactory.adsSerialPlcConnectionOf(serialDefinition, targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort);
         } else {
-            throw new PlcConnectionException("TCP DF1 connections not implemented yet.");
+            try {
+                return adsConnectionFactory.adsTcpPlcConnectionOf(InetAddress.getByName(host), port, targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort);
+            } catch (UnknownHostException e) {
+                throw new PlcConnectionException(e);
+            }
         }
     }
 
     @Override
     public PlcConnection connect(String url, PlcAuthentication authentication) throws PlcConnectionException {
-        throw new PlcConnectionException("DF1 connections doesn't support authentication.");
+        throw new PlcConnectionException("Basic ADS connections don't support authentication.");
+    }
+
+    public static AmsNetId AmsNetIdOf(String address) {
+        if (!AMS_NET_ID_PATTERN.matcher(address).matches()) {
+            throw new IllegalArgumentException(address + " must match " + AMS_NET_ID_PATTERN);
+        }
+        String[] split = address.split("\\.");
+        short[] shorts = ArrayUtils.toPrimitive(Stream.of(split).map(Integer::parseInt).map(Integer::shortValue).toArray(Short[]::new));
+        return new AmsNetId(shorts[5], shorts[4], shorts[3], shorts[2], shorts[1], shorts[0]);
     }
 
 }
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/Df1Field.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/Df1Field.java
deleted file mode 100644
index 762425a..0000000
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/Df1Field.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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 org.apache.plc4x.java.amsads;
-
-import com.sun.jna.StringArray;
-import org.apache.plc4x.java.api.model.PlcField;
-import org.apache.plc4x.java.df1.fields.DataType;
-
-public class Df1Field implements PlcField {
-
-    private final int address;
-    private final int size;
-    private final DataType dataType;
-    private final addressType address_type;
-
-    public Df1Field(int address, int size, DataType dataType, addressType address_type) {
-        this.address = address;
-        this.size = size;
-        this.dataType = dataType;
-        this.address_type = address_type;
-    }
-
-    public int getAddress() {
-        return address;
-    }
-
-    public int getSize() {
-        return size;
-    }
-
-    public DataType getDataType() {
-        return dataType;
-    }
-
-    public addressType getAddress_type() { return address_type; }
-
-    public static PlcField of(String fieldQuery) {
-        String[] tmp = fieldQuery.split(":");
-        return new Df1Field(Integer.parseInt(tmp[0]), DataType.valueOf(tmp[1]).getLength(),DataType.valueOf(tmp[1]), addressType.OFFSET);
-    }
-
-    public enum addressType {
-        OFFSET,
-        LOGICAL
-    }
-}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsAbstractPlcConnection.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsAbstractPlcConnection.java
new file mode 100644
index 0000000..2907acc
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsAbstractPlcConnection.java
@@ -0,0 +1,240 @@
+/*
+ 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 org.apache.plc4x.java.amsads.connection;
+
+import io.netty.channel.ChannelFuture;
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.configuration2.SystemConfiguration;
+import org.apache.plc4x.java.amsads.readwrite.*;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.api.messages.*;
+import org.apache.plc4x.java.base.connection.ChannelFactory;
+import org.apache.plc4x.java.base.connection.NettyPlcConnection;
+import org.apache.plc4x.java.base.messages.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.*;
+
+public abstract class AdsAbstractPlcConnection extends NettyPlcConnection implements PlcReader, PlcWriter, PlcProprietarySender {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(AdsAbstractPlcConnection.class);
+
+    protected static final Configuration CONF = new SystemConfiguration();
+    protected static final long SYMBOL_RESOLVE_TIMEOUT = CONF.getLong("plc4x.adsconnection.symbol.resolve,timeout", 3000);
+
+    protected final AmsNetId targetAmsNetId;
+
+    protected final int targetAmsPort;
+
+    protected final AmsNetId sourceAmsNetId;
+
+    protected final int sourceAmsPort;
+
+    protected final ConcurrentMap<SymbolicAdsField, DirectAdsField> fieldMapping;
+
+    protected AdsAbstractPlcConnection(ChannelFactory channelFactory, AmsNetId targetAmsNetId, int targetAmsPort) {
+        this(channelFactory, targetAmsNetId, targetAmsPort, generateAmsNetId(), generateAMSPort());
+    }
+
+    protected AdsAbstractPlcConnection(ChannelFactory channelFactory, AmsNetId targetAmsNetId, int targetAmsPort, AmsNetId sourceAmsNetId, int sourceAmsPort) {
+        super(channelFactory);
+        this.targetAmsNetId = targetAmsNetId;
+        this.targetAmsPort = targetAmsPort;
+        this.sourceAmsNetId = sourceAmsNetId;
+        this.sourceAmsPort = sourceAmsPort;
+        this.fieldMapping = new ConcurrentHashMap<>();
+    }
+
+    public AmsNetId getTargetAmsNetId() {
+        return targetAmsNetId;
+    }
+
+    public int getTargetAmsPort() {
+        return targetAmsPort;
+    }
+
+    public AmsNetId getSourceAmsNetId() {
+        return sourceAmsNetId;
+    }
+
+    public int getSourceAmsPort() {
+        return sourceAmsPort;
+    }
+
+    @Override
+    public boolean canRead() {
+        return true;
+    }
+
+    @Override
+    public boolean canWrite() {
+        return true;
+    }
+
+    @Override
+    public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest) {
+        mapFields(readRequest);
+        CompletableFuture<InternalPlcReadResponse> readFuture = new CompletableFuture<>();
+        ChannelFuture channelFuture = channel.writeAndFlush(new PlcRequestContainer<>((InternalPlcReadRequest) readRequest, readFuture));
+        channelFuture.addListener(future -> {
+            if (!future.isSuccess()) {
+                readFuture.completeExceptionally(future.cause());
+            }
+        });
+        return readFuture
+            .thenApply(PlcReadResponse.class::cast);
+    }
+
+    @Override
+    public PlcReadRequest.Builder readRequestBuilder() {
+        return new DefaultPlcReadRequest.Builder(this, new AdsPlcFieldHandler());
+    }
+
+    @Override
+    public PlcWriteRequest.Builder writeRequestBuilder() {
+        return new DefaultPlcWriteRequest.Builder(this, new AdsPlcFieldHandler());
+    }
+
+    @Override
+    public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest writeRequest) {
+        mapFields(writeRequest);
+        CompletableFuture<InternalPlcWriteResponse> writeFuture = new CompletableFuture<>();
+        ChannelFuture channelFuture = channel.writeAndFlush(new PlcRequestContainer<>((InternalPlcWriteRequest) writeRequest, writeFuture));
+        channelFuture.addListener(future -> {
+            if (!future.isSuccess()) {
+                writeFuture.completeExceptionally(future.cause());
+            }
+        });
+        return writeFuture
+            .thenApply(PlcWriteResponse.class::cast);
+    }
+
+    @Override
+    public <T> CompletableFuture<PlcProprietaryResponse<T>> send(PlcProprietaryRequest proprietaryRequest) {
+        CompletableFuture<InternalPlcProprietaryResponse<T>> sendFuture = new CompletableFuture<>();
+        ChannelFuture channelFuture = channel.writeAndFlush(new PlcRequestContainer<>((InternalPlcProprietaryRequest) proprietaryRequest, sendFuture));
+        channelFuture.addListener(future -> {
+            if (!future.isSuccess()) {
+                sendFuture.completeExceptionally(future.cause());
+            }
+        });
+        return sendFuture
+            .thenApply(PlcProprietaryResponse.class::cast);
+    }
+
+    protected void mapFields(PlcFieldRequest request) {
+        request.getFields().stream()
+            .parallel()
+            .filter(SymbolicAdsField.class::isInstance)
+            .map(SymbolicAdsField.class::cast)
+            .forEach(this::mapFields);
+    }
+
+    protected void mapFields(SymbolicAdsField symbolicAdsField) {
+        // If the map doesn't contain an entry for the given symbolicAdsField,
+        // resolve it and add it to the map.
+        fieldMapping.computeIfAbsent(symbolicAdsField, symbolicAdsFieldInternal -> {
+            LOGGER.debug("Resolving {}", symbolicAdsFieldInternal);
+            AdsReadWriteRequest adsReadWriteRequest = AdsReadWriteRequest.of(
+                targetAmsNetId,
+                targetAmsPort,
+                sourceAmsNetId,
+                sourceAmsPort,
+                Invoke.NONE,
+                IndexGroup.ReservedGroups.ADSIGRP_SYM_HNDBYNAME,
+                IndexOffset.NONE,
+                ReadLength.of(IndexOffset.NUM_BYTES),
+                Data.of(symbolicAdsFieldInternal.getSymbolicField())
+            );
+
+            // TODO: This is blocking, should be changed to be async.
+            CompletableFuture<InternalPlcProprietaryResponse<AdsReadWriteResponse>> getHandelFuture = new CompletableFuture<>();
+            channel.writeAndFlush(new PlcRequestContainer<>(new DefaultPlcProprietaryRequest<>(adsReadWriteRequest), getHandelFuture));
+            InternalPlcProprietaryResponse<AdsReadWriteResponse> getHandleResponse = getFromFuture(getHandelFuture, SYMBOL_RESOLVE_TIMEOUT);
+            AdsReadWriteResponse response = getHandleResponse.getResponse();
+
+            if (response.getResult().toAdsReturnCode() != AdsReturnCode.ADS_CODE_0) {
+                throw new PlcRuntimeException("Non error code received " + response.getResult());
+            }
+
+            IndexOffset symbolHandle = IndexOffset.of(response.getData().getBytes());
+            return DirectAdsField.of(IndexGroup.ReservedGroups.ADSIGRP_SYM_VALBYHND.getAsLong(), symbolHandle.getAsLong(), symbolicAdsFieldInternal.getAdsDataType(), symbolicAdsFieldInternal.getNumberOfElements());
+        });
+    }
+
+    protected static AmsNetId generateAmsNetId() {
+        return new AmsNetId((short) 0, (short) 0, (short) 0, (short) 0, (short) 0, (short) 0);
+    }
+
+    protected static int generateAMSPort() {
+        return 0;
+    }
+
+    @Override
+    public void close() throws PlcConnectionException {
+        fieldMapping.values().stream()
+            .parallel()
+            .map(adsField -> AdsWriteRequest.of(
+                targetAmsNetId,
+                targetAmsPort,
+                sourceAmsNetId,
+                sourceAmsPort,
+                Invoke.NONE,
+                IndexGroup.ReservedGroups.ADSIGRP_SYM_RELEASEHND,
+                IndexOffset.NONE,
+                Data.of(IndexGroup.of(adsField.getIndexGroup()).getBytes())
+            ))
+            .map(adsWriteRequest -> new PlcRequestContainer<>(new DefaultPlcProprietaryRequest<>(adsWriteRequest), new CompletableFuture<>()))
+            // We don't need a response so we just supply a throw away future.
+            .forEach(channel::write);
+        channel.flush();
+        super.close();
+    }
+
+    /**
+     * Clears the fieldMapping.
+     */
+    public void clearMapping() {
+        fieldMapping.clear();
+    }
+
+    protected <T> T getFromFuture(CompletableFuture<T> future, long timeout) {
+        try {
+            return future.get(timeout, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            LOGGER.warn("Interrupted!", e);
+            Thread.currentThread().interrupt();
+            throw new PlcRuntimeException(e);
+        } catch (ExecutionException | TimeoutException e) {
+            throw new PlcRuntimeException(e);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "AdsAbstractPlcConnection{" +
+            "targetAmsNetId=" + targetAmsNetId +
+            ", targetAmsPort=" + targetAmsPort +
+            ", sourceAmsNetId=" + sourceAmsNetId +
+            ", sourceAmsPort=" + sourceAmsPort +
+            "} " + super.toString();
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsConnectionFactory.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsConnectionFactory.java
new file mode 100644
index 0000000..4489d65
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsConnectionFactory.java
@@ -0,0 +1,57 @@
+/*
+ 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 org.apache.plc4x.java.amsads.connection;
+
+import org.apache.plc4x.java.amsads.readwrite.AmsNetId;
+
+import java.net.InetAddress;
+import java.util.Objects;
+
+public class AdsConnectionFactory {
+
+    public AdsTcpPlcConnection adsTcpPlcConnectionOf(InetAddress address, Integer port, AmsNetId targetAmsNetId, Integer targetAmsPort, AmsNetId sourceAmsNetId, Integer sourceAmsPort) {
+        Objects.requireNonNull(address);
+        Objects.requireNonNull(targetAmsNetId);
+        Objects.requireNonNull(targetAmsPort);
+        if (sourceAmsNetId == null || sourceAmsPort == null) {
+            if (port == null) {
+                return AdsTcpPlcConnection.of(address, targetAmsNetId, targetAmsPort);
+            } else {
+                return AdsTcpPlcConnection.of(address, port, targetAmsNetId, targetAmsPort);
+            }
+        } else {
+            if (port == null) {
+                return AdsTcpPlcConnection.of(address, targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort);
+            } else {
+                return AdsTcpPlcConnection.of(address, port, targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort);
+            }
+        }
+    }
+
+    public AdsSerialPlcConnection adsSerialPlcConnectionOf(String serialPort, AmsNetId targetAmsNetId, Integer targetAmsPort, AmsNetId sourceAmsNetId, Integer sourceAmsPort) {
+        Objects.requireNonNull(serialPort);
+        Objects.requireNonNull(targetAmsNetId);
+        Objects.requireNonNull(targetAmsPort);
+        if (sourceAmsNetId == null || sourceAmsPort == null) {
+            return AdsSerialPlcConnection.of(serialPort, targetAmsNetId, targetAmsPort);
+        } else {
+            return AdsSerialPlcConnection.of(serialPort, targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort);
+        }
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsSerialPlcConnection.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsSerialPlcConnection.java
new file mode 100644
index 0000000..3245afa
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsSerialPlcConnection.java
@@ -0,0 +1,68 @@
+/*
+ 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 org.apache.plc4x.java.amsads.connection;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import org.apache.plc4x.java.amsads.protocol.Ads2PayloadProtocol;
+import org.apache.plc4x.java.amsads.protocol.Payload2SerialProtocol;
+import org.apache.plc4x.java.amsads.protocol.Plc4x2AdsProtocol;
+import org.apache.plc4x.java.amsads.readwrite.AmsNetId;
+import org.apache.plc4x.java.base.protocol.SingleItemToSingleRequestProtocol;
+import org.apache.plc4x.java.serial.connection.connection.SerialChannelFactory;
+
+import java.util.concurrent.CompletableFuture;
+
+public class AdsSerialPlcConnection extends AdsAbstractPlcConnection {
+
+    private AdsSerialPlcConnection(String serialPort, AmsNetId targetAmsNetId, int targetAmsPort) {
+        this(serialPort, targetAmsNetId, targetAmsPort, generateAmsNetId(), generateAMSPort());
+    }
+
+    private AdsSerialPlcConnection(String serialPort, AmsNetId targetAmsNetId, int targetAmsPort, AmsNetId sourceAmsNetId, int sourceAmsPort) {
+        super(new SerialChannelFactory(serialPort), targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort);
+    }
+
+    public static AdsSerialPlcConnection of(String serialPort, AmsNetId targetAmsNetId, int targetAmsPort) {
+        return new AdsSerialPlcConnection(serialPort, targetAmsNetId, targetAmsPort);
+    }
+
+    public static AdsSerialPlcConnection of(String serialPort, AmsNetId targetAmsNetId, int targetAmsPort, AmsNetId sourceAmsNetId, int sourceAmsPort) {
+        return new AdsSerialPlcConnection(serialPort, targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort);
+    }
+
+    @Override
+    protected ChannelHandler getChannelHandler(CompletableFuture<Void> sessionSetupCompleteFuture) {
+        return new ChannelInitializer() {
+            @Override
+            protected void initChannel(Channel channel) {
+                // Build the protocol stack for communicating with the ads protocol.
+                ChannelPipeline pipeline = channel.pipeline();
+                pipeline.addLast(new Payload2SerialProtocol());
+                pipeline.addLast(new SingleMessageRateLimiter());
+                pipeline.addLast(new Ads2PayloadProtocol());
+                pipeline.addLast(new Plc4x2AdsProtocol(targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort, fieldMapping));
+                pipeline.addLast(new SingleItemToSingleRequestProtocol(AdsSerialPlcConnection.this, AdsSerialPlcConnection.this, null, timer));
+            }
+        };
+    }
+
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsTcpPlcConnection.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsTcpPlcConnection.java
new file mode 100644
index 0000000..278e542
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/AdsTcpPlcConnection.java
@@ -0,0 +1,369 @@
+/*
+ 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 org.apache.plc4x.java.amsads.connection;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.plc4x.java.amsads.protocol.Ads2PayloadProtocol;
+import org.apache.plc4x.java.amsads.protocol.Payload2TcpProtocol;
+import org.apache.plc4x.java.amsads.protocol.Plc4x2AdsProtocol;
+import org.apache.plc4x.java.amsads.readwrite.AmsNetId;
+import org.apache.plc4x.java.amsads.readwrite.AdsDeviceNotificationRequest;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.api.messages.*;
+import org.apache.plc4x.java.api.model.PlcConsumerRegistration;
+import org.apache.plc4x.java.api.model.PlcField;
+import org.apache.plc4x.java.api.model.PlcSubscriptionHandle;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.apache.plc4x.java.base.messages.*;
+import org.apache.plc4x.java.base.messages.items.BaseDefaultFieldItem;
+import org.apache.plc4x.java.base.model.DefaultPlcConsumerRegistration;
+import org.apache.plc4x.java.base.model.InternalPlcConsumerRegistration;
+import org.apache.plc4x.java.base.model.InternalPlcSubscriptionHandle;
+import org.apache.plc4x.java.base.model.SubscriptionPlcField;
+import org.apache.plc4x.java.base.protocol.SingleItemToSingleRequestProtocol;
+import org.apache.plc4x.java.tcp.connection.TcpSocketChannelFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class AdsTcpPlcConnection extends AdsAbstractPlcConnection implements PlcSubscriber {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(AdsTcpPlcConnection.class);
+
+    private static final int TCP_PORT = 48898;
+
+    private static final long ADD_DEVICE_TIMEOUT = CONF.getLong("plc4x.adsconnection.add.device,timeout", 3000);
+    private static final long DEL_DEVICE_TIMEOUT = CONF.getLong("plc4x.adsconnection.del.device,timeout", 3000);
+
+    private static AtomicInteger localPorts = new AtomicInteger(30000);
+
+    private Map<InternalPlcConsumerRegistration, Consumer<AdsDeviceNotificationRequest>> consumerRegistrations = new HashMap<>();
+
+    private AdsTcpPlcConnection(InetAddress address, AmsNetId targetAmsNetId, int targetint) {
+        this(address, targetAmsNetId, targetint, generateAmsNetId(), generateint());
+    }
+
+    private AdsTcpPlcConnection(InetAddress address, Integer port, AmsNetId targetAmsNetId, int targetint) {
+        this(address, port, targetAmsNetId, targetint, generateAmsNetId(), generateint());
+    }
+
+    private AdsTcpPlcConnection(InetAddress address, AmsNetId targetAmsNetId, int targetint, AmsNetId sourceAmsNetId, int sourceint) {
+        this(address, null, targetAmsNetId, targetint, sourceAmsNetId, sourceint);
+    }
+
+    private AdsTcpPlcConnection(InetAddress address, Integer port, AmsNetId targetAmsNetId, int targetint, AmsNetId sourceAmsNetId, int sourceint) {
+        super(new TcpSocketChannelFactory(address, port != null ? port : TCP_PORT), targetAmsNetId, targetint, sourceAmsNetId, sourceint);
+    }
+
+    public static AdsTcpPlcConnection of(InetAddress address, AmsNetId targetAmsNetId, int targetint) {
+        return new AdsTcpPlcConnection(address, targetAmsNetId, targetint);
+    }
+
+    public static AdsTcpPlcConnection of(InetAddress address, Integer port, AmsNetId targetAmsNetId, int targetint) {
+        return new AdsTcpPlcConnection(address, port, targetAmsNetId, targetint);
+    }
+
+    public static AdsTcpPlcConnection of(InetAddress address, AmsNetId targetAmsNetId, int targetint, AmsNetId sourceAmsNetId, int sourceint) {
+        return new AdsTcpPlcConnection(address, null, targetAmsNetId, targetint, sourceAmsNetId, sourceint);
+    }
+
+    public static AdsTcpPlcConnection of(InetAddress address, Integer port, AmsNetId targetAmsNetId, int targetint, AmsNetId sourceAmsNetId, int sourceint) {
+        return new AdsTcpPlcConnection(address, port, targetAmsNetId, targetint, sourceAmsNetId, sourceint);
+    }
+
+    @Override
+    protected ChannelHandler getChannelHandler(CompletableFuture<Void> sessionSetupCompleteFuture) {
+        return new ChannelInitializer() {
+            @Override
+            protected void initChannel(Channel channel) {
+                // Build the protocol stack for communicating with the ads protocol.
+                ChannelPipeline pipeline = channel.pipeline();
+                pipeline.addLast(new Payload2TcpProtocol());
+                pipeline.addLast(new Ads2PayloadProtocol());
+                pipeline.addLast(new Plc4x2AdsProtocol(targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort, fieldMapping));
+                pipeline.addLast(new SingleItemToSingleRequestProtocol(AdsTcpPlcConnection.this, AdsTcpPlcConnection.this, AdsTcpPlcConnection.this, timer, SingleItemToSingleRequestProtocol.SplitConfig.builder().dontSplitSubscribe().dontSplitUnsubscribe().build(), false));
+            }
+        };
+    }
+
+    public InetAddress getRemoteAddress() {
+        return ((TcpSocketChannelFactory) channelFactory).getAddress();
+    }
+
+    protected static AmsNetId generateAmsNetId() {
+        try {
+            return AmsNetId.of(Inet4Address.getLocalHost().getHostAddress() + ".1.1");
+        } catch (UnknownHostException e) {
+            throw new PlcRuntimeException(e);
+        }
+    }
+
+    protected static int generateint() {
+        return int.of(localPorts.getAndIncrement());
+    }
+
+    @Override
+    public CompletableFuture<PlcSubscriptionResponse> subscribe(PlcSubscriptionRequest plcSubscriptionRequest) {
+        InternalPlcSubscriptionRequest internalPlcSubscriptionRequest = checkInternal(plcSubscriptionRequest, InternalPlcSubscriptionRequest.class);
+        CompletableFuture<PlcSubscriptionResponse> future = new CompletableFuture<>();
+
+        Map<String, Pair<PlcResponseCode, PlcSubscriptionHandle>> responseItems = internalPlcSubscriptionRequest.getSubscriptionPlcFieldMap().entrySet().stream()
+            .map(subscriptionPlcFieldEntry -> {
+                final String plcFieldName = subscriptionPlcFieldEntry.getKey();
+                final SubscriptionPlcField subscriptionPlcField = subscriptionPlcFieldEntry.getValue();
+                final PlcField field = Objects.requireNonNull(subscriptionPlcField.getPlcField());
+
+                final IndexGroup indexGroup;
+                final IndexOffset indexOffset;
+                final AdsDataType adsDataType;
+                final int numberOfElements;
+                // If this is a symbolic field, it has to be resolved first.
+                // TODO: This is blocking, should be changed to be async.
+                if (field instanceof SymbolicAdsField) {
+                    mapFields((SymbolicAdsField) field);
+                    DirectAdsField directAdsField = fieldMapping.get(field);
+                    if (directAdsField == null) {
+                        throw new PlcRuntimeException("Unresolvable field " + field);
+                    }
+                    indexGroup = IndexGroup.of(directAdsField.getIndexGroup());
+                    indexOffset = IndexOffset.of(directAdsField.getIndexOffset());
+                    adsDataType = directAdsField.getAdsDataType();
+                    numberOfElements = directAdsField.getNumberOfElements();
+                }
+                // If it's no symbolic field, we can continue immediately
+                // without having to do any resolving.
+                else if (field instanceof DirectAdsField) {
+                    DirectAdsField directAdsField = (DirectAdsField) field;
+                    indexGroup = IndexGroup.of(directAdsField.getIndexGroup());
+                    indexOffset = IndexOffset.of(directAdsField.getIndexOffset());
+                    adsDataType = directAdsField.getAdsDataType();
+                    numberOfElements = directAdsField.getNumberOfElements();
+                } else {
+                    throw new IllegalArgumentException("Unsupported field type " + field.getClass());
+                }
+
+                final TransmissionMode transmissionMode;
+                long cycleTime = 4000000;
+                switch (subscriptionPlcField.getPlcSubscriptionType()) {
+                    case CYCLIC:
+                        transmissionMode = TransmissionMode.DefinedValues.ADSTRANS_SERVERCYCLE;
+                        cycleTime = subscriptionPlcField.getDuration().orElse(Duration.ofSeconds(1)).toMillis();
+                        break;
+                    case CHANGE_OF_STATE:
+                        transmissionMode = TransmissionMode.DefinedValues.ADSTRANS_SERVERONCHA;
+                        break;
+                    default:
+                        throw new PlcRuntimeException("Unmapped type " + subscriptionPlcField.getPlcSubscriptionType());
+                }
+
+                // Prepare the subscription request itself.
+                AdsAddDeviceNotificationRequest adsAddDeviceNotificationRequest = AdsAddDeviceNotificationRequest.of(
+                    targetAmsNetId,
+                    targetint,
+                    sourceAmsNetId,
+                    sourceint,
+                    Invoke.NONE,
+                    indexGroup,
+                    indexOffset,
+                    Length.of(adsDataType.getTargetByteSize() * (long) numberOfElements),
+                    transmissionMode,
+                    // We set max delay to cycle time as we don't have a second parameter for this in the plc4j-api
+                    MaxDelay.of(cycleTime + 1),
+                    CycleTime.of(cycleTime)
+                );
+
+                // Send the request to the plc and wait for a response
+                // TODO: This is blocking, should be changed to be async.
+                CompletableFuture<InternalPlcProprietaryResponse<AdsAddDeviceNotificationResponse>> addDeviceFuture = new CompletableFuture<>();
+                channel.writeAndFlush(new PlcRequestContainer<>(new DefaultPlcProprietaryRequest<>(adsAddDeviceNotificationRequest), addDeviceFuture));
+                InternalPlcProprietaryResponse<AdsAddDeviceNotificationResponse> addDeviceResponse = getFromFuture(addDeviceFuture, ADD_DEVICE_TIMEOUT);
+                AdsAddDeviceNotificationResponse response = addDeviceResponse.getResponse();
+
+                // Abort if we got anything but a successful response.
+                if (response.getResult().toAdsReturnCode() != AdsReturnCode.ADS_CODE_0) {
+                    throw new PlcRuntimeException("Error code received " + response.getResult());
+                }
+                PlcSubscriptionHandle adsSubscriptionHandle = new AdsSubscriptionHandle(this, plcFieldName, adsDataType, response.getNotificationHandle());
+                return Pair.of(plcFieldName, Pair.of(PlcResponseCode.OK, adsSubscriptionHandle));
+            })
+            .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
+
+        future.complete(new DefaultPlcSubscriptionResponse(internalPlcSubscriptionRequest, responseItems));
+        return future;
+    }
+
+    @Override
+    public CompletableFuture<PlcUnsubscriptionResponse> unsubscribe(PlcUnsubscriptionRequest plcUnsubscriptionRequest) {
+        InternalPlcUnsubscriptionRequest internalPlcUnsubscriptionRequest = checkInternal(plcUnsubscriptionRequest, InternalPlcUnsubscriptionRequest.class);
+        for (InternalPlcSubscriptionHandle internalPlcSubscriptionHandle : internalPlcUnsubscriptionRequest.getInternalPlcSubscriptionHandles()) {
+            if (internalPlcSubscriptionHandle instanceof AdsSubscriptionHandle) {
+                AdsSubscriptionHandle adsSubscriptionHandle = (AdsSubscriptionHandle) internalPlcSubscriptionHandle;
+                AdsDeleteDeviceNotificationRequest adsDeleteDeviceNotificationRequest =
+                    AdsDeleteDeviceNotificationRequest.of(
+                        targetAmsNetId,
+                        targetint,
+                        sourceAmsNetId,
+                        sourceint,
+                        Invoke.NONE,
+                        adsSubscriptionHandle.getNotificationHandle()
+                    );
+                CompletableFuture<InternalPlcProprietaryResponse<AdsDeleteDeviceNotificationResponse>> deleteDeviceFuture =
+                    new CompletableFuture<>();
+                channel.writeAndFlush(new PlcRequestContainer<>(new DefaultPlcProprietaryRequest<>(adsDeleteDeviceNotificationRequest), deleteDeviceFuture));
+
+                InternalPlcProprietaryResponse<AdsDeleteDeviceNotificationResponse> deleteDeviceResponse =
+                    getFromFuture(deleteDeviceFuture, DEL_DEVICE_TIMEOUT);
+                AdsDeleteDeviceNotificationResponse response = deleteDeviceResponse.getResponse();
+                if (response.getResult().toAdsReturnCode() != AdsReturnCode.ADS_CODE_0) {
+                    throw new PlcRuntimeException("Non error code received " + response.getResult());
+                }
+            }
+        }
+        CompletableFuture<PlcUnsubscriptionResponse> future = new CompletableFuture<>();
+        future.complete(new DefaultPlcUnsubscriptionResponse(internalPlcUnsubscriptionRequest));
+        return future;
+    }
+
+    @Override
+    public PlcConsumerRegistration register(Consumer<PlcSubscriptionEvent> consumer, Collection<PlcSubscriptionHandle> handles) {
+        return register(consumer, handles.toArray(new PlcSubscriptionHandle[0]));
+    }
+
+    public InternalPlcConsumerRegistration register(Consumer<PlcSubscriptionEvent> consumer, PlcSubscriptionHandle... handles) {
+        Objects.requireNonNull(consumer);
+        Objects.requireNonNull(handles);
+        InternalPlcSubscriptionHandle[] internalPlcSubscriptionHandles = new InternalPlcSubscriptionHandle[handles.length];
+        for (int i = 0; i < handles.length; i++) {
+            internalPlcSubscriptionHandles[i] = checkInternal(handles[i], InternalPlcSubscriptionHandle.class);
+        }
+
+        InternalPlcConsumerRegistration internalPlcConsumerRegistration = new DefaultPlcConsumerRegistration(this, consumer, internalPlcSubscriptionHandles);
+        Map<NotificationHandle, AdsSubscriptionHandle> notificationHandleAdsSubscriptionHandleMap = Arrays.stream(internalPlcSubscriptionHandles)
+            .map(subscriptionHandle -> checkInternal(subscriptionHandle, AdsSubscriptionHandle.class))
+            .collect(Collectors.toConcurrentMap(AdsSubscriptionHandle::getNotificationHandle, Function.identity()));
+
+        Consumer<AdsDeviceNotificationRequest> adsDeviceNotificationRequestConsumer =
+            adsDeviceNotificationRequest -> adsDeviceNotificationRequest.getAdsStampHeaders().forEach(adsStampHeader -> {
+                Instant timeStamp = adsStampHeader.getTimeStamp().getAsDate().toInstant();
+
+                Map<String, Pair<PlcResponseCode, BaseDefaultFieldItem>> fields = new HashMap<>();
+                adsStampHeader.getAdsNotificationSamples()
+                    .forEach(adsNotificationSample -> {
+                        NotificationHandle notificationHandle = adsNotificationSample.getNotificationHandle();
+                        Data data = adsNotificationSample.getData();
+                        AdsSubscriptionHandle adsSubscriptionHandle = notificationHandleAdsSubscriptionHandleMap.get(notificationHandle);
+                        if (adsSubscriptionHandle == null) {
+                            // TODO: we might want to refactor this so that we don't subscribe to everything in the first place.
+                            // TODO: rather than we add a Consumer with the handle as key
+                            LOGGER.trace("We are not interested in this sample {} with handle {}", adsNotificationSample, notificationHandle);
+                            return;
+                        }
+                        String plcFieldName = adsSubscriptionHandle.getPlcFieldName();
+                        AdsDataType adsDataType = adsSubscriptionHandle.getAdsDataType();
+                        try {
+                            BaseDefaultFieldItem baseDefaultFieldItem = LittleEndianDecoder.decodeData(adsDataType, data.getBytes());
+                            fields.put(plcFieldName, Pair.of(PlcResponseCode.OK, baseDefaultFieldItem));
+                        } catch (RuntimeException e) {
+                            LOGGER.error("Can't decode {}", data, e);
+                        }
+
+                    });
+                try {
+                    PlcSubscriptionEvent subscriptionEventItem = new DefaultPlcSubscriptionEvent(timeStamp, fields);
+                    consumer.accept(subscriptionEventItem);
+                } catch (RuntimeException e) {
+                    LOGGER.error("Can't dispatch adsStampHeader {}", adsStampHeader, e);
+                }
+            });
+
+        // Store the reference for so it can be uses for later
+        consumerRegistrations.put(internalPlcConsumerRegistration, adsDeviceNotificationRequestConsumer);
+        // register the actual consumer.
+        getChannel().pipeline().get(Plc4x2AdsProtocol.class).addConsumer(adsDeviceNotificationRequestConsumer);
+
+        return internalPlcConsumerRegistration;
+    }
+
+    @Override
+    public void unregister(PlcConsumerRegistration plcConsumerRegistration) {
+        InternalPlcConsumerRegistration internalPlcConsumerRegistration = checkInternal(plcConsumerRegistration, InternalPlcConsumerRegistration.class);
+        Consumer<AdsDeviceNotificationRequest> adsDeviceNotificationRequestConsumer = consumerRegistrations.remove(internalPlcConsumerRegistration);
+        if (adsDeviceNotificationRequestConsumer == null) {
+            return;
+        }
+        getChannel().pipeline().get(Plc4x2AdsProtocol.class).removeConsumer(adsDeviceNotificationRequestConsumer);
+    }
+
+    @Override
+    public boolean canSubscribe() {
+        return true;
+    }
+
+    @Override
+    public PlcSubscriptionRequest.Builder subscriptionRequestBuilder() {
+        return new DefaultPlcSubscriptionRequest.Builder(this, new AdsPlcFieldHandler());
+    }
+
+    @Override
+    public PlcUnsubscriptionRequest.Builder unsubscriptionRequestBuilder() {
+        return new DefaultPlcUnsubscriptionRequest.Builder(this);
+    }
+
+    @Override
+    public void close() throws PlcConnectionException {
+        try {
+            consumerRegistrations.values().forEach(getChannel().pipeline().get(Plc4x2AdsProtocol.class)::removeConsumer);
+            List<PlcSubscriptionHandle> collect = consumerRegistrations.keySet().stream()
+                .map(InternalPlcConsumerRegistration::getAssociatedHandles)
+                .flatMap(Collection::stream)
+                .map(PlcSubscriptionHandle.class::cast)
+                .collect(Collectors.toList());
+
+            PlcUnsubscriptionRequest plcUnsubscriptionRequest = new DefaultPlcUnsubscriptionRequest.Builder(this).addHandles(collect).build();
+            unsubscribe(plcUnsubscriptionRequest).get(5, TimeUnit.SECONDS);
+
+            consumerRegistrations.clear();
+        } catch (InterruptedException e) {
+            LOGGER.warn("Exception while closing", e);
+            Thread.currentThread().interrupt();
+        } catch (RuntimeException | ExecutionException | TimeoutException e) {
+            LOGGER.warn("Exception while closing", e);
+        }
+        super.close();
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/SerialAMSADSConnection.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/SerialAMSADSConnection.java
deleted file mode 100644
index 4eded14..0000000
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/SerialAMSADSConnection.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- 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 org.apache.plc4x.java.amsads.connection;
-
-import io.netty.channel.*;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.plc4x.java.api.messages.PlcReadRequest;
-import org.apache.plc4x.java.api.messages.PlcReadResponse;
-import org.apache.plc4x.java.api.messages.PlcWriteRequest;
-import org.apache.plc4x.java.api.messages.PlcWriteResponse;
-import org.apache.plc4x.java.base.connection.ChannelFactory;
-import org.apache.plc4x.java.base.connection.SerialChannelFactory;
-import org.apache.plc4x.java.base.events.ConnectedEvent;
-import org.apache.plc4x.java.base.messages.*;
-import org.apache.plc4x.java.df1.protocol.Df1Protocol;
-import org.apache.plc4x.java.df1.protocol.Plc4XDf1Protocol;
-import org.apache.plc4x.java.df1.util.Df1FieldHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.concurrent.CompletableFuture;
-
-public class SerialAMSADSConnection extends BaseAMSADSConnection implements PlcReader, PlcWriter {
-
-    private static final Logger logger = LoggerFactory.getLogger(SerialAMSADSConnection.class);
-
-    private short localAddr;
-    private short remoteAddr;
-
-    public SerialAMSADSConnection(String comPortName, String params) {
-        this(new SerialChannelFactory(comPortName), params);
-    }
-
-    public SerialAMSADSConnection(ChannelFactory channelFactory, String params) {
-        super(channelFactory, false);
-        this.localAddr = (short) 0x00;
-        this.remoteAddr = (short) 0x09;
-
-        // Override some of the settings, if they are asked for.
-        if (!StringUtils.isEmpty(params)) {
-            for (String param : params.split("&")) {
-                String[] paramElements = param.split("=");
-                String paramName = paramElements[0];
-                if (paramElements.length == 2) {
-                    String paramValue = paramElements[1];
-                    switch (paramName) {
-                        case "local-addr":
-                            this.localAddr = Short.parseShort(paramValue);
-                            break;
-                        case "remote-addr":
-                            this.remoteAddr = Short.parseShort(paramValue);
-                            break;
-                        default:
-                            logger.debug("Unknown parameter {} with value {}", paramName, paramValue);
-                    }
-                }
-            }
-        }
-    }
-
-    @Override
-    public boolean canRead() {
-        return true;
-    }
-
-    @Override
-    public boolean canWrite() {
-        return true;
-    }
-
-    @Override
-    protected ChannelHandler getChannelHandler(CompletableFuture<Void> sessionSetupCompleteFuture) {
-        return new ChannelInitializer() {
-            @Override
-            protected void initChannel(Channel channel) {
-                // Build the protocol stack for communicating with the s7 protocol.
-                ChannelPipeline pipeline = channel.pipeline();
-                pipeline.addLast(new ChannelInboundHandlerAdapter() {
-                    @Override
-                    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
-                        if (evt instanceof ConnectedEvent) {
-                            sessionSetupCompleteFuture.complete(null);
-                        } else {
-                            super.userEventTriggered(ctx, evt);
-                        }
-                    }
-                });
-                pipeline.addLast(new Df1Protocol(localAddr, remoteAddr));
-                pipeline.addLast(new Plc4XDf1Protocol());
-            }
-        };
-    }
-
-    @Override
-    public PlcReadRequest.Builder readRequestBuilder() {
-        return new DefaultPlcReadRequest.Builder(this, new Df1FieldHandler());
-    }
-
-    @Override
-    public PlcWriteRequest.Builder writeRequestBuilder() {
-        return new DefaultPlcWriteRequest.Builder(this, new Df1FieldHandler());
-    }
-
-    @Override
-    public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest) {
-        CompletableFuture<InternalPlcReadResponse> future = new CompletableFuture<>();
-        PlcRequestContainer<InternalPlcReadRequest, InternalPlcReadResponse> container =
-            new PlcRequestContainer<>((InternalPlcReadRequest) readRequest, future);
-        channel.writeAndFlush(container).addListener(f -> {
-            if (!f.isSuccess()) {
-                future.completeExceptionally(f.cause());
-            }
-        });
-        return future
-            .thenApply(PlcReadResponse.class::cast);
-    }
-
-    @Override
-    public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest writeRequest) {
-        CompletableFuture<InternalPlcWriteResponse> future = new CompletableFuture<>();
-        PlcRequestContainer<InternalPlcWriteRequest, InternalPlcWriteResponse> container =
-            new PlcRequestContainer<>((InternalPlcWriteRequest) writeRequest, future);
-        channel.writeAndFlush(container).addListener(f -> {
-            if (!f.isSuccess()) {
-                future.completeExceptionally(f.cause());
-            }
-        });
-        return future
-            .thenApply(PlcWriteResponse.class::cast);
-    }
-
-}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsDataType.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsDataType.java
new file mode 100644
index 0000000..b60706c
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsDataType.java
@@ -0,0 +1,577 @@
+/*
+ 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 org.apache.plc4x.java.amsads.model;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
+
+/**
+ * Documentation can be found here:
+ *
+ * @see <a href="https://infosys.beckhoff.com/english.php?content=../content/1033/tcsystemmanager/basics/TcSysMgr_DatatypeComparison.htm&id=">TwinCAT System Manager - I/O Variables</a>
+ * @see <a href="https://infosys.beckhoff.com/english.php?content=../content/1033/tcplccontrol/html/tcplcctrl_plc_data_types_overview.htm&id">TwinCAT PLC Control: Data Types</a>
+ */
+public enum AdsDataType {
+    // TODO: maybe this are just types for the plc ide and can be removed
+    // https://infosys.beckhoff.com/english.php?content=../content/1033/tcsystemmanager/basics/TcSysMgr_DatatypeComparison.htm&id=
+    BIT(0, 1, 8),
+    BIT8((short) 0x00, (short) 0xFF, 8),
+    BITARR8((short) 0x00, (short) 0xFF, 8),
+    BITARR16(0x0000, 0xFFFF, 16),
+    BITARR32(0x0000_0000L, 0xFFFF_FFFFL, 32),
+    INT8(Byte.MIN_VALUE, Byte.MAX_VALUE, 8),
+    INT16(Short.MIN_VALUE, Short.MAX_VALUE, 16),
+    INT32(Integer.MIN_VALUE, Integer.MAX_VALUE, 32),
+    INT64(Long.MIN_VALUE, Long.MAX_VALUE, 64),
+    UINT8((short) 0, (short) 0xFF, 8),
+    UINT16(0, 0xFFFF, 16),
+    UINT32(0L, 0xFFFF_FFFFL, 32),
+    UINT64(BigInteger.ZERO, produceUnsignedMaxValue(8), 64),
+    FLOAT(-Float.MAX_VALUE, Float.MAX_VALUE, 32),
+    DOUBLE(-Double.MAX_VALUE, Double.MAX_VALUE, 64),
+    // https://infosys.beckhoff.com/english.php?content=../content/1033/tcplccontrol/html/tcplcctrl_plc_data_types_overview.htm&id
+    // Standard Data Types
+    /**
+     * BOOL type variables may be given the values TRUE and FALSE.
+     * <p>
+     * Type	Memory use
+     * BOOL	8 Bit
+     * Note:
+     * <p>
+     * A BOOL type variable is true, if the least significant bit in the memory is set (e.g. 2#00000001 ). If no bit is set in the memory, the variable is FALSE (2#00000000). All other values can´t be interpeted accurately and be displayed (***INVALID: 16#xy *** in the Online View). Such problems may appear, if for example overlapped memory ranges are used in the PLC program.
+     * <p>
+     * Example:
+     * <p>
+     * The boolean variable is in the same memory range as the byte variable.
+     */
+    BOOL((byte) 0, (byte) 1, 8),
+    /**
+     * BYTE
+     * <p>
+     * Integer data type.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * BYTE	0	255	8 Bit
+     */
+    BYTE((short) 0, (short) 255, 8),
+    /**
+     * WORD
+     * Integer data type.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * WORD	0	65535	16 Bit
+     */
+    WORD(0, 65535, 16),
+    /**
+     * DWORD
+     * Integer data type.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * DWORD	0	4294967295	32 Bit
+     */
+    DWORD(0L, 4294967295L, 32),
+    /**
+     * SINT
+     * (Short) signed integer data type.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * SINT	-128	127	8 Bit
+     */
+    SINT(Byte.MIN_VALUE, Byte.MAX_VALUE, 8),
+    /**
+     * USINT
+     * Unsigned (short) integer data type.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * USINT	0	255	8 Bit
+     */
+    USINT((short) 0, (short) 0xFF, 8),
+    /**
+     * INT
+     * Signed integer data type.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * INT	-32768	32767	16 Bit
+     */
+    INT(Short.MIN_VALUE, Short.MAX_VALUE, 16),
+    /**
+     * UINT
+     * Unsigned integer data type.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * UINT	0	65535	16 Bit
+     */
+    UINT(0, 65535, 16),
+    /**
+     * DINT
+     * Signed integer data type.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * DINT	-2147483648	2147483647	32 Bit
+     */
+    DINT(-2147483648, 2147483647, 32),
+    /**
+     * UDINT
+     * Unsigned integer data type.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * UDINT	0	4294967295	32 Bit
+     */
+    UDINT(0L, 4294967295L, 32),
+    /**
+     * LINT  (64 bit integer, currently not supported by TwinCAT)
+     */
+    LINT(Long.MIN_VALUE, Long.MAX_VALUE, 64),
+    /**
+     * ULINT (Unsigned 64 bit integer, currently not supported by TwinCAT)
+     */
+    ULINT(BigInteger.ZERO, produceUnsignedMaxValue(8), 64),
+    /**
+     * REAL
+     * 32 Bit floating point data type. It is required to represent rational numbers.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * REAL	~ -3.402823 x 1038	~ 3.402823 x 1038	32 Bit
+     */
+    REAL(-Float.MAX_VALUE, Float.MAX_VALUE, 32),
+    /**
+     * LREAL
+     * 64 Bit floating point data type. It is required to represent rational numbers.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * LREAL	~ -1.79769313486231E308	~ 1.79769313486232E308	64 Bit
+     */
+    LREAL(-Double.MAX_VALUE, Double.MAX_VALUE, 64),
+    /**
+     * STRING
+     * A STRING type variable can contain any string of characters. The size entry in the declaration determines how much memory space should be reserved for the variable. It refers to the number of characters in the string and can be placed in parentheses or square brackets.
+     * <p>
+     * Example of a string declaration with 35 characters:
+     * <p>
+     * str:STRING(35):='This is a String';
+     * Type	Memory use
+     * STRING
+     * If no size specification is given, the default size of 80 characters will be used: Memory use [Bytes] =  80 + 1 Byte for string terminated Null character;
+     * If string size specification is given: Memory use [Bytes] = String Size + 1 Byte for string terminated Null character);
+     */
+    STRING(81 * 8),
+    /**
+     * TIME
+     * Duration time. The most siginificant digit is one millisecond. The data type is handled internally like DWORD.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * TIME	T#0ms	T#71582m47s295ms	32 Bit
+     */
+    TIME(0L, Duration.ofMinutes(71582).plusSeconds(47).plusMillis(295).toMillis(), 32),
+    /**
+     * TIME_OF_DAY
+     * TOD
+     * Time of day. The most siginificant digit is one millisecond. The data type is handled internally like DWORD.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * TIME_OF_DAY
+     * TOD
+     * <p>
+     * TOD#00:00	TOD#1193:02:47.295	32 Bit
+     *///TODO: strange maximum
+    TIME_OF_DAY(0L, ChronoUnit.MILLIS.between(LocalTime.of(0, 0), LocalTime.of(23, 59, 59)), 32),
+    /**
+     * DATE
+     * Date. The most significant digit is one second. The data type is handled internally like DWORD.
+     * <p>
+     * Type	Lower bound	Upper bound	Memory use
+     * DATE	D#1970-01-01	D#2106-02-06	32 Bit
+     */
+    DATE(0L, TimeUnit.DAYS.toSeconds(ChronoUnit.DAYS.between(LocalDate.of(1970, 1, 1), LocalDate.of(2106, 2, 6))), 32),
+    /**
+     * DATE_AND_TIME
+     * DT
+     * Date and time. The most siginificant digit is one second. The data type is handled internally like DWORD.
+     * <p>
+     * Type	Lower  bound	Upper  bound	Memory use
+     * DATE_AND_TIME
+     * DT
+     * <p>
+     * DT#1970-01-01-00:00	DT#2106-02-06-06:28:15	32 Bit
+     */
+    DATE_AND_TIME(0L, ChronoUnit.SECONDS.between(LocalDateTime.of(1970, 1, 1, 0, 0), LocalDateTime.of(2106, 2, 6, 6, 28, 15)), 32),
+    //User-defined Data Types
+    /**
+     * Arrays
+     * One-, two-, and three-dimensional fields (arrays) are supported as elementary data types. Arrays can be defined both in the declaration part of a POU and in the global variable lists.
+     * <p>
+     * Syntax:
+     * <p>
+     * &lt;Field_Name&gt;:ARRAY [&lt;LowLim1&gt;..&lt;UpLim1&gt;, &lt;LowLim2&gt;..&lt;UpLim2&gt;] OF &lt;elem. Type&gt;
+     * <p>
+     * LowLim1, LowLim2 identify the lower limit of the field range; UpLim1 and UpLim2 identify the upper limit. The range values must be integers.
+     * <p>
+     * Example:
+     * <p>
+     * Card_game: ARRAY [1..13, 1..4] OF INT;
+     * <p>
+     * <p>
+     * <p>
+     * Initializing of Arrays
+     * You can initialize either all of the elements in an array or none of them.
+     * <p>
+     * Example for initializing arrays:
+     * <p>
+     * arr1 : ARRAY [1..5] OF INT := 1,2,3,4,5;
+     * arr2 : ARRAY [1..2,3..4] OF INT := 1,3(7); (* short for 1,7,7,7 *)
+     * arr3 : ARRAY [1..2,2..3,3..4] OF INT := 2(0),4(4),2,3; (* short for 0,0,4,4,4,4,2,3 *)
+     * <p>
+     * <p>
+     * <p>
+     * Example for the initialization of an array of a structure:
+     * <p>
+     * TYPE STRUCT1
+     * STRUCT
+     * p1:int;
+     * p2:int;
+     * p3:dword;
+     * END_STRUCT
+     * arr1 : ARRAY[1..3] OF STRUCT1:= (p1:=1,p2:=10,p3:=4723), (p1:=2,p2:=0,p3:=299), (p1:=14,p2:=5,p3:=112);
+     * <p>
+     * <p>
+     * Example of the partial initialization of an Array:
+     * <p>
+     * arr1 : ARRAY [1..10] OF INT := 1,2;
+     * Elements to which no value is pre-assigned are initialized with the default initial value of the basic type. In the example above, the elements arr1[3]  to arr1[10] are therefore initialized with 0.
+     * <p>
+     * <p>
+     * <p>
+     * Array components are accessed in a two-dimensional array using the following syntax:
+     * <p>
+     * &lt;Field_Name&gt;[Index1,Index2]
+     * <p>
+     * Example:
+     * <p>
+     * Card_game[9,2]
+     * <p>
+     * <p>
+     * <p>
+     * Note:
+     * <p>
+     * If you define a function in your project with the name CheckBounds, you can automatically check for out-of-range errors in arrays ! The name of the function is fixed and can only have this designation.
+     */
+    ARRAY(-1),//TODO: implement me
+    /**
+     * Pointer
+     * Variable or function block addresses are saved in pointers while a program is running. Pointer declarations have the following syntax:
+     * <p>
+     * &lt;Identifier&gt;: POINTER TO &lt;Datatype/Functionblock&gt;;
+     * A pointer can point to any data type or function block even to user-defined types. The function of the Address Operator ADR is to assign the address of a variable or function block to the pointer.
+     * A pointer can be dereferenced by adding the content operator "^" after the pointer identifier. With the help of the SIZEOF Operator, e.g. a pointer increment can be done.
+     * <p>
+     * <p>
+     * Please note: A pointer is counted up byte-wise ! You can get it counted up like it is usual in the C-Compiler by using the instruction p=p+SIZEOF(p^);.
+     * <p>
+     * <p>
+     * <p>
+     * Attention:
+     * After an Online Change there might be changes concerning the data on certain addresses. Please regard this in case of using pointers on addresses.
+     * <p>
+     * <p>
+     * <p>
+     * Example:
+     * <p>
+     * pt:POINTER TO INT;
+     * var_int1:INT := 5;
+     * var_int2:INT;
+     * <p>
+     * <p>
+     * pt := ADR(var_int1);
+     * var_int2:= pt^; (* var_int2 is now 5 *)
+     * <p>
+     * <p>
+     * Example 2 (Pointer increment):
+     * <p>
+     * ptByCurrDataOffs : POINTER TO BYTE;
+     * udiAddress       : UDINT;
+     * <p>
+     * <p>
+     * <p>
+     * (*--- pointer increment ---*)
+     * udiAddress := ptByCurrDataOffs;
+     * udiAddress := udiAddress + SIZEOF(ptByCurrDataOffs^);
+     * ptByCurrDataOffs := udiAddress;
+     * (* -- end of pointer increment ---*)
+     */
+    POINTER(-1),//TODO: implement me,
+    /**
+     * Enumeration (ENUM)
+     * Enumeration is a user-defined data type that is made up of a number of string constants. These constants are referred to as enumeration values. Enumeration values are recognized in all areas of the project even if they were locally declared within aPOU. It is best to create your enumerations as objects in the Object Organizer under the register card Data types. They begin with the keyword TYPE and end with END_TYPE.
+     * <p>
+     * Syntax:
+     * <p>
+     * TYPE &lt;Identifier&gt;:(&lt;Enum_0&gt; ,&lt;Enum_1&gt;, ...,&lt;Enum_n&gt;);END_TYPE
+     * <p>
+     * The &lt;Identifier&gt; can take on one of the enumeration values and will be initialized with the first one. These values are compatible with whole numbers which means that you can perform operations with them just as you would with INT. You can assign a number x to the &lt;Identifier&gt;. If the enumeration values are not initialized, counting will begin with 0. When initializing, make certain the initial values are increasing. The validity of the number will be reviewed at the t [...]
+     * <p>
+     * Example:
+     * <p>
+     * TRAFFIC_SIGNAL: (Red, Yellow, Green:=10); (*The initial value for each of the colors is red 0, yellow 1, green 10 *)
+     * TRAFFIC_SIGNAL:=0; (* The value of the traffic signal is red*)
+     * FOR i:= Red TO Green DO
+     * i := i + 1;
+     * END_FOR;
+     * <p>
+     * You may not use the same enumeration value more than once.
+     * <p>
+     * Example:
+     * <p>
+     * TRAFFIC_SIGNAL: (red, yellow, green);
+     * COLOR: (blue, white, red);
+     * <p>
+     * Error: red may not be used for both TRAFFIC_SIGNAL and COLOR.
+     */
+    ENUM(-1),//TODO: implement me,
+    /**
+     * Structures (STRUCT)
+     * Structures are created as objects in the Object Organizer under the register card Data types. They begin with the keyword TYPE and end with END_TYPE.The syntax for structure declarations is as follows:
+     * <p>
+     * TYPE &lt;Structurename&gt;:
+     * STRUCT
+     * &lt;Declaration of Variables 1&gt;
+     * .
+     * .
+     * &lt;Declaration of Variables n&gt;
+     * END_STRUCT
+     * END_TYPE
+     * <p>
+     * &lt;Structurename&gt; is a type that is recognized throughout the project and can be used like a standard data type. Interlocking structures are allowed. The only restriction is that variables may not be placed at addresses (the AT declaration is not allowed!).
+     * <p>
+     * Example for a structure definition named Polygonline:
+     * <p>
+     * TYPE Polygonline:
+     * STRUCT
+     * Start:ARRAY [1..2] OF INT;
+     * Point1:ARRAY [1..2] OF INT;
+     * Point2:ARRAY [1..2] OF INT;
+     * Point3:ARRAY [1..2] OF INT;
+     * Point4:ARRAY [1..2] OF INT;
+     * End:ARRAY [1..2] OF INT;
+     * END_STRUCT
+     * END_TYPE
+     * <p>
+     * You can gain access to structure components using the following syntax:
+     * <p>
+     * &lt;Structure_Name&gt;.&lt;Componentname&gt;
+     * <p>
+     * For example, if you have a structure named "Week" that contains a component named "Monday", you can get to it by doing the following: Week.Monday
+     * <p>
+     * <p>
+     * <p>
+     * Note:
+     * Due to different alignments, structures and arrays may have different configurations and sizes on different hardware platforms (e.g. CX1000 and CX90xx).
+     * <p>
+     * During data exchange the size and structure alignment must be identical!
+     * <p>
+     * <p>
+     * <p>
+     * Example for a structure definition with name ST_ALIGN_SAMPLE:
+     * <p>
+     * TYPE ST_ALIGN_SAMPLE:
+     * STRUCT
+     * _diField1   : DINT;
+     * _byField1   : BYTE;
+     * _iField     : INT;
+     * _byField2   : BYTE;
+     * _diField2   : DINT;
+     * _pField     : POINTER TO BYTE;
+     * END_STRUCT
+     * END_TYPE
+     * <p>
+     * On CX90xx (RISC) platforms the member components of structure ST_ALIGN_SAMPLE have the following sizes and offsets:
+     * <p>
+     * _diField1 (DINT), Offset = 0 (16#0),   Size = 4
+     * _byField1 (BYTE), Offset = 4 (16#4),   Size = 1
+     * _iField (INT), Offset = 6 (16#6),   Size = 2
+     * _byField2 (BYTE), Offset = 8 (16#8),   Size = 1
+     * _diField2 (DINT), Offset = 12 (16#C),  Size = 4
+     * _pField (POINTER TO BYTE), Offset = 16 (16#10), Size = 4
+     * <p>
+     * Overall size through natural alignment with Pack(4) and so-called padding bytes: 20
+     * <p>
+     * <p>
+     * <p>
+     * On CX10xx platforms the member components of structure ST_ALIGN_SAMPLE have the following sizes and offsets:
+     * <p>
+     * _diField1 (DINT), Offset = 0 (16#0),   Size = 4
+     * _byField1 (BYTE), Offset = 4 (16#4),   Size = 1
+     * _iField (INT), Offset = 5 (16#5),   Size = 2
+     * _byField2 (BYTE), Offset = 7 (16#7),   Size = 1
+     * _diField2 (DINT), Offset = 8 (16#8),  Size = 4
+     * _pField (POINTER TO BYTE), Offset = 12 (16#C), Size = 4
+     * <p>
+     * Overall size: 16
+     * <p>
+     * <p>
+     * <p>
+     * Display of structure ST_ALIGN_SAMPLE for CX90xx platforms (RISC) with representation of the padding bytes:
+     * <p>
+     * TYPE ST_ALIGN_SAMPLE:
+     * STRUCT
+     * _diField1    : DINT;
+     * _byField1    : BYTE;
+     * _byPadding   : BYTE;
+     * _iField      : INT;
+     * _byField2    : BYTE;
+     * _a_byPadding : ARRAY[0..2] OF BYTE;
+     * _diField2    : DINT;
+     * _pField      : POINTER TO BYTE;
+     * END_STRUCT
+     * END_TYPE
+     */
+    STRUCT(-1),//TODO: implement me,
+    /**
+     * References (Alias types)
+     * You can use the user-defined derived data type to create an alternative name for a variable, constant or function block. Create your references as objects in the Object Organizer under the register card Data types. They begin with the keyword TYPE and end with END_TYPE.
+     * <p>
+     * Syntax:
+     * <p>
+     * TYPE &lt;Identifier&gt;: &lt;Assignment term&gt;;
+     * END_TYPE
+     * <p>
+     * Example:
+     * <p>
+     * TYPE message:STRING[50];
+     * END_TYPE;
+     */
+    ALIAS(-1),//TODO: implement me,
+    /**
+     * Subrange types
+     * A sub-range data type is a type whose range of values is only a subset of that of the basic type. The declaration can be carried out in the data types register, but a variable can also be directly declared with a subrange type:
+     * Syntax for the declaration in the 'Data types' register:
+     * <p>
+     * TYPE &lt;Name&gt; : &lt;Inttype&gt; (&lt;ug&gt;..&lt;og&gt;) END_TYPE;
+     * Type	Description
+     * &lt;Name&gt;	must be a valid IEC identifier
+     * &lt;Inttype&gt;	is one of the data types SINT, USINT, INT, UINT, DINT, UDINT, BYTE, WORD, DWORD (LINT, ULINT, LWORD).
+     * &lt;ug&gt;	Is a constant which must be compatible with the basic type and which sets the lower boundary of the range types. The lower boundary itself is included in this range.
+     * &lt;og&gt;	Is a constant that must be compatible with the basic type, and sets the upper boundary of the range types. The upper boundary itself is included in this basic type.
+     * Example:
+     * <p>
+     * TYPE
+     * SubInt : INT (-4095..4095);
+     * END_TYPE
+     * Direct declaration of a variable with a subrange type:
+     * <p>
+     * VAR
+     * i1 : INT (-4095..4095);
+     * i2: INT (5...10):=5;
+     * ui : UINT (0..10000);
+     * END_VAR
+     * If a constant is assigned to a subrange type (in the declaration or in the implementation) that does not apply to this range (e.g. 1:=5000), an error message is issued.
+     * In order to check for observance of range boundaries at runtime, the functions CheckRangeSigned or CheckRangeUnsigned must be introduced.
+     */
+    SUB_RANGE_DATA_TYPE(-1),//TODO: implement me,
+
+    UNKNOWN(-1);
+
+    private final String typeName;
+
+    private final Number lowerBound;
+
+    private final double lowerBoundDouble;
+
+    private final Number upperBound;
+
+    private final double upperBoundDouble;
+
+    private final int memoryUse;
+
+    private final int targetByteSize;
+
+    // TODO: BYTE.MAX default might not be the best....
+    AdsDataType(int memoryUse) {
+        this(0, Byte.MAX_VALUE, memoryUse);
+    }
+
+    AdsDataType(Number lowerBound, Number upperBound, int memoryUse) {
+        this.lowerBound = lowerBound;
+        this.lowerBoundDouble = lowerBound.doubleValue();
+        this.upperBound = upperBound;
+        this.upperBoundDouble = upperBound.doubleValue();
+        this.typeName = name();
+        this.memoryUse = memoryUse;
+        this.targetByteSize = this.memoryUse / 8;
+    }
+
+    public String getTypeName() {
+        return typeName;
+    }
+
+    public Number getLowerBound() {
+        return lowerBound;
+    }
+
+    public Number getUpperBound() {
+        return upperBound;
+    }
+
+    public int getMemoryUse() {
+        return memoryUse;
+    }
+
+    public int getTargetByteSize() {
+        return targetByteSize;
+    }
+
+    public boolean withinBounds(double other) {
+        return other >= lowerBoundDouble && other <= upperBoundDouble;
+    }
+
+    @Override
+    public String toString() {
+        return "AdsDataType{" +
+            "typeName='" + typeName + '\'' +
+            ", lowerBound=" + lowerBound +
+            ", upperBound=" + upperBound +
+            ", memoryUse=" + memoryUse +
+            ", targetByteSize=" + targetByteSize +
+            "} " + super.toString();
+    }
+
+    private static BigInteger produceUnsignedMaxValue(int numberOfBytes) {
+        return new BigInteger(
+            ArrayUtils.insert(
+                0,
+                IntStream.range(0, numberOfBytes)
+                    .map(ignore -> 0xff)
+                    .collect(
+                        ByteArrayOutputStream::new,
+                        (baos, i) -> baos.write((byte) i),
+                        (baos1, baos2) -> baos1.write(baos2.toByteArray(), 0, baos2.size())
+                    )
+                    .toByteArray(),
+                (byte) 0x0)
+        );
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/fields/DataType.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsField.java
similarity index 56%
copy from sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/fields/DataType.java
copy to sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsField.java
index 97b8b17..a6a12a3 100644
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/fields/DataType.java
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsField.java
@@ -7,7 +7,7 @@
  "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
+   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
@@ -16,29 +16,11 @@
  specific language governing permissions and limitations
  under the License.
  */
-package org.apache.plc4x.java.amsads.fields;
+package org.apache.plc4x.java.amsads.model;
 
-public enum DataType {
-    BIT((short)1),
-    BIT_STRING((short)-1),
-    BYTE_STRING((short)-1),
-    INTEGER((short)2),
-    TIMER((short)-1),
-    COUNTER((short)-1),
-    GENERAL_COUNT_STRUCTURE((short)-1),
-    FLOAT((short)-1),
-    ARRAY((short)-1),
-    ADDRESS((short)-1),
-    BINARY_CODED_DECIMAL((short)-1);
-
-    private final short length;
-
-    DataType(short length) {
-        this.length = length;
-    }
-
-    public short getLength() {
-        return length;
-    }
+import org.apache.plc4x.java.api.model.PlcField;
 
+@FunctionalInterface
+public interface AdsField extends PlcField {
+    AdsDataType getAdsDataType();
 }
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsPlcFieldHandler.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsPlcFieldHandler.java
new file mode 100644
index 0000000..6c8e060
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsPlcFieldHandler.java
@@ -0,0 +1,1015 @@
+/*
+ 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 org.apache.plc4x.java.amsads.model;
+
+import org.apache.plc4x.java.api.exceptions.PlcInvalidFieldException;
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.api.model.PlcField;
+import org.apache.plc4x.java.base.connection.DefaultPlcFieldHandler;
+import org.apache.plc4x.java.base.messages.items.*;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+// TODO: implement me acording to ads. currently copy pasta from S7
+// Use endian decoders.
+// TODO: replace all ifs with switches
+public class AdsPlcFieldHandler extends DefaultPlcFieldHandler {
+
+    @Override
+    public PlcField createField(String fieldQuery) throws PlcInvalidFieldException {
+        if (DirectAdsField.matches(fieldQuery)) {
+            return DirectAdsField.of(fieldQuery);
+        } else if (SymbolicAdsField.matches(fieldQuery)) {
+            return SymbolicAdsField.of(fieldQuery);
+        }
+        throw new PlcInvalidFieldException(fieldQuery);
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeBoolean(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        // All of these types are declared as Bit or Bit-String types.
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalEncodeBoolean(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeByte(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalEncodeInteger(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeShort(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalEncodeInteger(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeInteger(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalEncodeInteger(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeBigInteger(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalEncodeInteger(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeLong(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalEncodeInteger(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeFloat(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalEncodeFloatingPoint(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeDouble(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalEncodeFloatingPoint(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeString(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalEncodeString(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeTime(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalTimeTemporal(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeDate(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalDateTemporal(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    @Override
+    public BaseDefaultFieldItem encodeDateTime(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BIT:
+            case BIT8:
+            case BITARR8:
+            case BITARR16:
+            case BITARR32:
+            case INT8:
+            case INT16:
+            case INT32:
+            case INT64:
+            case UINT8:
+            case UINT16:
+            case UINT32:
+            case UINT64:
+            case FLOAT:
+            case DOUBLE:
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+            case SINT:
+            case USINT:
+            case INT:
+            case UINT:
+            case DINT:
+            case UDINT:
+            case LINT:
+            case ULINT:
+            case REAL:
+            case LREAL:
+            case STRING:
+            case TIME:
+            case TIME_OF_DAY:
+            case DATE:
+            case DATE_AND_TIME:
+            case ARRAY:
+            case POINTER:
+            case ENUM:
+            case STRUCT:
+            case ALIAS:
+            case SUB_RANGE_DATA_TYPE:
+                return internalDateTimeTemporal(field, values);
+            case UNKNOWN:
+            default:
+                throw new PlcRuntimeException("Invalid encoder for type " + adsField.getAdsDataType().name());
+        }
+    }
+
+    private BaseDefaultFieldItem internalEncodeBoolean(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case BOOL:
+            case BYTE:
+            case WORD:
+            case DWORD:
+                break;
+            default:
+                throw new IllegalArgumentException(
+                    "Cannot assign boolean values to " + adsField.getAdsDataType().name() + " fields.");
+        }
+        List<Boolean> booleanValues = new LinkedList<>();
+        for (Object value : values) {
+            if (value instanceof Boolean) {
+                Boolean booleanValue = (Boolean) value;
+                booleanValues.add(booleanValue);
+            } else if (value instanceof Byte) {
+                Byte byteValue = (Byte) value;
+                BitSet bitSet = BitSet.valueOf(new byte[]{byteValue});
+                for (int i = 0; i < 8; i++) {
+                    booleanValues.add(bitSet.get(i));
+                }
+            } else if (value instanceof Short) {
+                Short shortValue = (Short) value;
+                BitSet bitSet = BitSet.valueOf(new long[]{shortValue});
+                for (int i = 0; i < 16; i++) {
+                    booleanValues.add(bitSet.get(i));
+                }
+            } else if (value instanceof Integer) {
+                Integer integerValue = (Integer) value;
+                BitSet bitSet = BitSet.valueOf(new long[]{integerValue});
+                for (int i = 0; i < 32; i++) {
+                    booleanValues.add(bitSet.get(i));
+                }
+            } else if (value instanceof Long) {
+                long longValue = (Long) value;
+                BitSet bitSet = BitSet.valueOf(new long[]{longValue});
+                for (int i = 0; i < 64; i++) {
+                    booleanValues.add(bitSet.get(i));
+                }
+            } else {
+                throw new IllegalArgumentException(
+                    "Value of type " + value.getClass().getName() +
+                        " is not assignable to " + adsField.getAdsDataType().name() + " fields.");
+            }
+        }
+        return new DefaultBooleanFieldItem(booleanValues.toArray(new Boolean[0]));
+    }
+
+    private BaseDefaultFieldItem internalEncodeInteger(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        Class<? extends BaseDefaultFieldItem> fieldType;
+        switch (adsField.getAdsDataType()) {
+            case BYTE:
+                fieldType = DefaultByteFieldItem.class;
+                break;
+            case WORD:
+                fieldType = DefaultByteArrayFieldItem.class;
+                break;
+            case DWORD:
+                fieldType = DefaultByteArrayFieldItem.class;
+                break;
+            case SINT:
+                fieldType = DefaultIntegerFieldItem.class;
+                break;
+            case USINT:
+                fieldType = DefaultLongFieldItem.class;
+                break;
+            case INT:
+                fieldType = DefaultShortFieldItem.class;
+                break;
+            case UINT:
+                fieldType = DefaultIntegerFieldItem.class;
+                break;
+            case DINT:
+                fieldType = DefaultIntegerFieldItem.class;
+                break;
+            case UDINT:
+                fieldType = DefaultLongFieldItem.class;
+                break;
+            case LINT:
+                fieldType = DefaultLongFieldItem.class;
+                break;
+            case ULINT:
+                fieldType = DefaultBigIntegerFieldItem.class;
+                break;
+            case INT32:
+                fieldType = DefaultIntegerFieldItem.class;
+                break;
+            case INT64:
+                fieldType = DefaultLongFieldItem.class;
+                break;
+            default:
+                throw new IllegalArgumentException(
+                    "Cannot assign integer values to " + adsField.getAdsDataType().name() + " fields.");
+        }
+        if (fieldType == DefaultLongFieldItem.class) {
+            Long[] longValues = new Long[values.length];
+            for (int i = 0; i < values.length; i++) {
+                if (!((values[i] instanceof Byte) || (values[i] instanceof Short) ||
+                    (values[i] instanceof Integer) || (values[i] instanceof BigInteger) || (values[i] instanceof Long))) {
+                    throw new IllegalArgumentException(
+                        "Value of type " + values[i].getClass().getName() +
+                            " is not assignable to " + adsField.getAdsDataType().name() + " fields.");
+                }
+
+                Number value = (Number) values[i];
+                if (!adsField.getAdsDataType().withinBounds(value.doubleValue())) {
+                    throw new IllegalArgumentException("Value " + values[i] + " ist not within bounds of " + adsField.getAdsDataType());
+                }
+                longValues[i] = value.longValue();
+            }
+            return new DefaultLongFieldItem(longValues);
+        } else {
+            BigInteger[] bigIntegerValues = new BigInteger[values.length];
+            for (int i = 0; i < values.length; i++) {
+                BigDecimal value;
+                if (values[i] instanceof BigInteger) {
+                    value = new BigDecimal((BigInteger) values[i]);
+                } else if (((values[i] instanceof Byte) || (values[i] instanceof Short) ||
+                    (values[i] instanceof Integer) || (values[i] instanceof Long))) {
+                    value = new BigDecimal(((Number) values[i]).longValue());
+                } else {
+                    throw new IllegalArgumentException(
+                        "Value of type " + values[i].getClass().getName() +
+                            " is not assignable to " + adsField.getAdsDataType().name() + " fields.");
+                }
+                if (!adsField.getAdsDataType().withinBounds(value.doubleValue())) {
+                    throw new IllegalArgumentException("Value " + values[i] + " ist not within bounds of " + adsField.getAdsDataType());
+                }
+                bigIntegerValues[i] = value.toBigInteger();
+            }
+            return new DefaultBigIntegerFieldItem(bigIntegerValues);
+        }
+    }
+
+    private BaseDefaultFieldItem internalEncodeFloatingPoint(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        Class<? extends BaseDefaultFieldItem> fieldType;
+        AdsDataType adsDataType = adsField.getAdsDataType();
+        switch (adsDataType) {
+            case REAL:
+                fieldType = DefaultFloatFieldItem.class;
+                break;
+            case LREAL:
+                fieldType = DefaultDoubleFieldItem.class;
+                break;
+            default:
+                throw new IllegalArgumentException(
+                    "Cannot assign floating point values to " + adsDataType.name() + " fields.");
+        }
+        if (fieldType == DefaultDoubleFieldItem.class) {
+            Double[] floatingPointValues = new Double[values.length];
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] instanceof Float) {
+                    floatingPointValues[i] = ((Float) values[i]).doubleValue();
+                } else if (values[i] instanceof Double) {
+                    floatingPointValues[i] = (Double) values[i];
+                } else {
+                    throw new IllegalArgumentException(
+                        "Value of type " + values[i].getClass().getName() +
+                            " is not assignable to " + adsDataType.name() + " fields.");
+                }
+
+                Number value = (Number) values[i];
+                if (!adsDataType.withinBounds(value.doubleValue())) {
+                    throw new IllegalArgumentException("Value " + values[i] + " ist not within bounds of " + adsDataType);
+                }
+            }
+            return new DefaultDoubleFieldItem(floatingPointValues);
+        } else {
+            Float[] floatingPointValues = new Float[values.length];
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] instanceof Double) {
+                    Double aDouble = (Double) values[i];
+                    if (!adsDataType.withinBounds(aDouble)) {
+                        throw new IllegalArgumentException(
+                            "Value of " + aDouble + " exceeds allowed minimum for type "
+                                + adsDataType.name() + " (min " + adsDataType.getLowerBound() + "/max +" + adsDataType.getUpperBound() + ")");
+                    }
+                    floatingPointValues[i] = aDouble.floatValue();
+                } else if (values[i] instanceof Float) {
+                    floatingPointValues[i] = (Float) values[i];
+                } else {
+                    throw new IllegalArgumentException(
+                        "Value of type " + values[i].getClass().getName() +
+                            " is not assignable to " + adsDataType.name() + " fields.");
+                }
+
+                Number value = (Number) values[i];
+                if (!adsDataType.withinBounds(value.doubleValue())) {
+                    throw new IllegalArgumentException("Value " + values[i] + " ist not within bounds of " + adsDataType);
+                }
+            }
+            return new DefaultFloatFieldItem(floatingPointValues);
+        }
+    }
+
+    private BaseDefaultFieldItem internalEncodeString(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        Number maxLength = adsField.getAdsDataType().getUpperBound();
+        //boolean encoding16Bit;
+        switch (adsField.getAdsDataType()) {
+            case STRING:
+                //encoding16Bit = false;
+                break;
+            default:
+                throw new IllegalArgumentException(
+                    "Cannot assign string values to " + adsField.getAdsDataType().name() + " fields.");
+        }
+        List<String> stringValues = new LinkedList<>();
+        for (Object value : values) {
+            if (value instanceof String) {
+                String stringValue = (String) value;
+                if (stringValue.length() > maxLength.intValue()) {
+                    throw new IllegalArgumentException(
+                        "String length " + stringValue.length() + " exceeds allowed maximum for type "
+                            + adsField.getAdsDataType().name() + " (max " + maxLength + ")");
+                }
+                stringValues.add(stringValue);
+            }
+            // All other types just translate to max one String character.
+            else if (value instanceof Byte) {
+                Byte byteValue = (Byte) value;
+                byte[] stringBytes = new byte[]{byteValue};
+                /*if (encoding16Bit) {
+                    stringValues.add(new String(stringBytes, StandardCharsets.UTF_16));
+                } else {*/
+                stringValues.add(new String(stringBytes, StandardCharsets.UTF_8));
+                /*}*/
+            } else if (value instanceof Short) {
+                Short shortValue = (Short) value;
+                byte[] stringBytes = new byte[2];
+                stringBytes[0] = (byte) (shortValue >> 8);
+                stringBytes[1] = (byte) (shortValue & 0xFF);
+                /*if (encoding16Bit) {
+                    stringValues.add(new String(stringBytes, StandardCharsets.UTF_16));
+                } else {*/
+                stringValues.add(new String(stringBytes, StandardCharsets.UTF_8));
+                /*}*/
+            } else if (value instanceof Integer) {
+                Integer integerValue = (Integer) value;
+                byte[] stringBytes = new byte[4];
+                stringBytes[0] = (byte) ((integerValue >> 24) & 0xFF);
+                stringBytes[1] = (byte) ((integerValue >> 16) & 0xFF);
+                stringBytes[2] = (byte) ((integerValue >> 8) & 0xFF);
+                stringBytes[3] = (byte) (integerValue & 0xFF);
+                /*if (encoding16Bit) {
+                    stringValues.add(new String(stringBytes, StandardCharsets.UTF_16));
+                } else {*/
+                stringValues.add(new String(stringBytes, StandardCharsets.UTF_8));
+                /*}*/
+            } else if (value instanceof Long) {
+                Long longValue = (Long) value;
+                byte[] stringBytes = new byte[8];
+                stringBytes[0] = (byte) ((longValue >> 56) & 0xFF);
+                stringBytes[1] = (byte) ((longValue >> 48) & 0xFF);
+                stringBytes[2] = (byte) ((longValue >> 40) & 0xFF);
+                stringBytes[3] = (byte) ((longValue >> 32) & 0xFF);
+                stringBytes[4] = (byte) ((longValue >> 24) & 0xFF);
+                stringBytes[5] = (byte) ((longValue >> 16) & 0xFF);
+                stringBytes[6] = (byte) ((longValue >> 8) & 0xFF);
+                stringBytes[7] = (byte) (longValue & 0xFF);
+                /*if (encoding16Bit) {
+                    stringValues.add(new String(stringBytes, StandardCharsets.UTF_16));
+                } else {*/
+                stringValues.add(new String(stringBytes, StandardCharsets.UTF_8));
+                /*}*/
+            } else {
+                throw new IllegalArgumentException(
+                    "Value of type " + value.getClass().getName() +
+                        " is not assignable to " + adsField.getAdsDataType().name() + " fields.");
+            }
+        }
+        return new DefaultStringFieldItem(stringValues.toArray(new String[0]));
+    }
+
+    private BaseDefaultFieldItem internalTimeTemporal(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case TIME:
+            case DATE:
+            case DATE_AND_TIME:
+                break;
+            default:
+                throw new IllegalArgumentException(
+                    "Cannot assign temporal values to " + adsField.getAdsDataType().name() + " fields.");
+        }
+        // TODO: support other types
+        List<LocalTime> localTimeValues = Arrays.stream(values)
+            .filter(LocalTime.class::isInstance)
+            .map(LocalTime.class::cast)
+            .collect(Collectors.toList());
+        return new DefaultLocalTimeFieldItem(localTimeValues.toArray(new LocalTime[0]));
+    }
+
+    private BaseDefaultFieldItem internalDateTemporal(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        switch (adsField.getAdsDataType()) {
+            case TIME:
+            case DATE:
+            case DATE_AND_TIME:
+                break;
+            default:
+                throw new IllegalArgumentException(
+                    "Cannot assign temporal values to " + adsField.getAdsDataType().name() + " fields.");
+        }
+        // TODO: support other types
+        List<LocalDate> localDateValues = Arrays.stream(values)
+            .filter(LocalDate.class::isInstance)
+            .map(LocalDate.class::cast)
+            .collect(Collectors.toList());
+        return new DefaultLocalDateFieldItem(localDateValues.toArray(new LocalDate[0]));
+    }
+
+    private BaseDefaultFieldItem internalDateTimeTemporal(PlcField field, Object[] values) {
+        AdsField adsField = (AdsField) field;
+        Class<? extends BaseDefaultFieldItem> fieldType;
+        switch (adsField.getAdsDataType()) {
+            case TIME:
+                fieldType = DefaultLocalTimeFieldItem.class;
+                break;
+            case DATE:
+                fieldType = DefaultLocalDateFieldItem.class;
+                break;
+            case DATE_AND_TIME:
+                fieldType = DefaultLocalDateTimeFieldItem.class;
+                break;
+            default:
+                throw new IllegalArgumentException(
+                    "Cannot assign temporal values to " + adsField.getAdsDataType().name() + " fields.");
+        }
+        // TODO: add type conversion
+        if (fieldType == DefaultLocalDateTimeFieldItem.class) {
+            return new DefaultLocalDateTimeFieldItem(Arrays.stream(values)
+                .filter(LocalDateTime.class::isInstance)
+                .map(LocalDateTime.class::cast).toArray(LocalDateTime[]::new));
+        } else if (fieldType == DefaultLocalDateFieldItem.class) {
+            return new DefaultLocalDateFieldItem(Arrays.stream(values)
+                .filter(LocalDate.class::isInstance)
+                .map(LocalDate.class::cast).toArray(LocalDate[]::new));
+        } else {
+            return new DefaultLocalTimeFieldItem(Arrays.stream(values)
+                .filter(LocalTime.class::isInstance)
+                .map(LocalTime.class::cast).toArray(LocalTime[]::new));
+        }
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsSubscriptionHandle.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsSubscriptionHandle.java
new file mode 100644
index 0000000..3d04c5e
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/AdsSubscriptionHandle.java
@@ -0,0 +1,85 @@
+/*
+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 org.apache.plc4x.java.amsads.model;
+
+import org.apache.plc4x.java.ads.api.commands.types.NotificationHandle;
+import org.apache.plc4x.java.base.messages.PlcSubscriber;
+import org.apache.plc4x.java.base.model.DefaultPlcSubscriptionHandle;
+
+import java.util.Objects;
+
+public class AdsSubscriptionHandle extends DefaultPlcSubscriptionHandle {
+
+    private final String plcFieldName;
+
+    private final AdsDataType adsDataType;
+
+    private final NotificationHandle notificationHandle;
+
+    public AdsSubscriptionHandle(PlcSubscriber plcSubscriber, String plcFieldName, AdsDataType adsDataType, NotificationHandle notificationHandle) {
+        super(plcSubscriber);
+        this.plcFieldName = plcFieldName;
+        this.adsDataType = adsDataType;
+        this.notificationHandle = notificationHandle;
+    }
+
+    public String getPlcFieldName() {
+        return plcFieldName;
+    }
+
+    public AdsDataType getAdsDataType() {
+        return adsDataType;
+    }
+
+    public NotificationHandle getNotificationHandle() {
+        return notificationHandle;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof AdsSubscriptionHandle)) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+        AdsSubscriptionHandle that = (AdsSubscriptionHandle) o;
+        return Objects.equals(plcFieldName, that.plcFieldName) &&
+            adsDataType == that.adsDataType &&
+            Objects.equals(notificationHandle, that.notificationHandle);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), plcFieldName, adsDataType, notificationHandle);
+    }
+
+    @Override
+    public String toString() {
+        return "AdsSubscriptionHandle{" +
+            "plcFieldName='" + plcFieldName + '\'' +
+            ", adsDataType=" + adsDataType +
+            ", notificationHandle=" + notificationHandle +
+            "} " + super.toString();
+    }
+
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/DirectAdsField.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/DirectAdsField.java
new file mode 100644
index 0000000..ebfe0ee
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/DirectAdsField.java
@@ -0,0 +1,141 @@
+/*
+ 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 org.apache.plc4x.java.amsads.model;
+
+import org.apache.plc4x.java.ads.api.util.ByteValue;
+import org.apache.plc4x.java.api.exceptions.PlcInvalidFieldException;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * ADS address witch is defined by {@code indexGroup/indexOffset}. These values can be either supplied as int or hex
+ * representation.
+ */
+public class DirectAdsField implements AdsField {
+
+    private static final Pattern RESOURCE_ADDRESS_PATTERN = Pattern.compile("^((0[xX](?<indexGroupHex>[0-9a-fA-F]+))|(?<indexGroup>\\d+))/((0[xX](?<indexOffsetHex>[0-9a-fA-F]+))|(?<indexOffset>\\d+)):(?<adsDataType>\\w+)(\\[(?<numberOfElements>\\d)])?");
+
+    private final long indexGroup;
+
+    private final long indexOffset;
+
+    private final AdsDataType adsDataType;
+
+    private final int numberOfElements;
+
+    private DirectAdsField(long indexGroup, long indexOffset, AdsDataType adsDataType, Integer numberOfElements) {
+        ByteValue.checkUnsignedBounds(indexGroup, 4);
+        this.indexGroup = indexGroup;
+        ByteValue.checkUnsignedBounds(indexOffset, 4);
+        this.indexOffset = indexOffset;
+        this.adsDataType = Objects.requireNonNull(adsDataType);
+        this.numberOfElements = numberOfElements != null ? numberOfElements : 1;
+        if (this.numberOfElements <= 0) {
+            throw new IllegalArgumentException("numberOfElements must be greater then zero. Was " + this.numberOfElements);
+        }
+    }
+
+    public static DirectAdsField of(long indexGroup, long indexOffset, AdsDataType adsDataType, Integer numberOfElements) {
+        return new DirectAdsField(indexGroup, indexOffset, adsDataType, numberOfElements);
+    }
+
+    public static DirectAdsField of(String address) {
+        Matcher matcher = RESOURCE_ADDRESS_PATTERN.matcher(address);
+        if (!matcher.matches()) {
+            throw new PlcInvalidFieldException(address, RESOURCE_ADDRESS_PATTERN, "{indexGroup}/{indexOffset}:{adsDataType}([numberOfElements])?");
+        }
+
+        String indexGroupStringHex = matcher.group("indexGroupHex");
+        String indexGroupString = matcher.group("indexGroup");
+
+        String indexOffsetStringHex = matcher.group("indexOffsetHex");
+        String indexOffsetString = matcher.group("indexOffset");
+
+        long indexGroup;
+        if (indexGroupStringHex != null) {
+            indexGroup = Long.parseLong(indexGroupStringHex, 16);
+        } else {
+            indexGroup = Long.parseLong(indexGroupString);
+        }
+
+        long indexOffset;
+        if (indexOffsetStringHex != null) {
+            indexOffset = Long.parseLong(indexOffsetStringHex, 16);
+        } else {
+            indexOffset = Long.parseLong(indexOffsetString);
+        }
+
+        String adsDataTypeString = matcher.group("adsDataType");
+        AdsDataType adsDataType = AdsDataType.valueOf(adsDataTypeString);
+
+        String numberOfElementsString = matcher.group("numberOfElements");
+        Integer numberOfElements = numberOfElementsString != null ? Integer.valueOf(numberOfElementsString) : null;
+
+        return new DirectAdsField(indexGroup, indexOffset, adsDataType, numberOfElements);
+    }
+
+    public static boolean matches(String address) {
+        return RESOURCE_ADDRESS_PATTERN.matcher(address).matches();
+    }
+
+    public long getIndexGroup() {
+        return indexGroup;
+    }
+
+    public long getIndexOffset() {
+        return indexOffset;
+    }
+
+    @Override
+    public AdsDataType getAdsDataType() {
+        return adsDataType;
+    }
+
+    public int getNumberOfElements() {
+        return numberOfElements;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof DirectAdsField)) {
+            return false;
+        }
+        DirectAdsField that = (DirectAdsField) o;
+        return indexGroup == that.indexGroup &&
+            indexOffset == that.indexOffset;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(indexGroup, indexOffset);
+    }
+
+    @Override
+    public String toString() {
+        return "DirectAdsField{" +
+            "indexGroup=" + indexGroup +
+            ", indexOffset=" + indexOffset +
+            '}';
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/SymbolicAdsField.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/SymbolicAdsField.java
new file mode 100644
index 0000000..6959d9c
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/SymbolicAdsField.java
@@ -0,0 +1,105 @@
+/*
+ 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 org.apache.plc4x.java.amsads.model;
+
+import org.apache.plc4x.java.api.exceptions.PlcInvalidFieldException;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * ADS address witch is defined by symbolic name (e.g. {@code Main.items[0]}).
+ */
+public class SymbolicAdsField implements AdsField {
+
+    private static final Pattern SYMBOLIC_ADDRESS_PATTERN = Pattern.compile("^(?<symbolicAddress>.+):(?<adsDataType>\\w+)(\\[(?<numberOfElements>\\d)])?");
+
+    private final String symbolicAddress;
+
+    private final AdsDataType adsDataType;
+
+    private final int numberOfElements;
+
+    private SymbolicAdsField(String symbolicAddress, AdsDataType adsDataType, Integer numberOfElements) {
+        this.symbolicAddress = Objects.requireNonNull(symbolicAddress);
+        this.adsDataType = Objects.requireNonNull(adsDataType);
+        this.numberOfElements = numberOfElements != null ? numberOfElements : 1;
+        if (this.numberOfElements <= 0) {
+            throw new IllegalArgumentException("numberOfElements must be greater then zero. Was " + this.numberOfElements);
+        }
+    }
+
+    public static SymbolicAdsField of(String address) {
+        Matcher matcher = SYMBOLIC_ADDRESS_PATTERN.matcher(address);
+        if (!matcher.matches()) {
+            throw new PlcInvalidFieldException(address, SYMBOLIC_ADDRESS_PATTERN, "{address}");
+        }
+        String symbolicAddress = matcher.group("symbolicAddress");
+
+        String adsDataTypeString = matcher.group("adsDataType");
+        AdsDataType adsDataType = AdsDataType.valueOf(adsDataTypeString);
+
+        String numberOfElementsString = matcher.group("numberOfElements");
+        Integer numberOfElements = numberOfElementsString != null ? Integer.valueOf(numberOfElementsString) : null;
+
+        return new SymbolicAdsField(symbolicAddress, adsDataType, numberOfElements);
+    }
+
+    public static boolean matches(String address) {
+        return SYMBOLIC_ADDRESS_PATTERN.matcher(address).matches();
+    }
+
+    public String getSymbolicField() {
+        return symbolicAddress;
+    }
+
+    @Override
+    public AdsDataType getAdsDataType() {
+        return adsDataType;
+    }
+
+    public int getNumberOfElements() {
+        return numberOfElements;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof SymbolicAdsField)) {
+            return false;
+        }
+        SymbolicAdsField that = (SymbolicAdsField) o;
+        return Objects.equals(symbolicAddress, that.symbolicAddress);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(symbolicAddress);
+    }
+
+    @Override
+    public String toString() {
+        return "SymbolicAdsField{" +
+            "symbolicAddress='" + symbolicAddress + '\'' +
+            '}';
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/BaseAMSADSConnection.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/package-info.java
similarity index 56%
rename from sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/BaseAMSADSConnection.java
rename to sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/package-info.java
index f1bdf79..b07e8f5 100644
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/connection/BaseAMSADSConnection.java
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/model/package-info.java
@@ -7,7 +7,7 @@
  "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
+   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
@@ -16,19 +16,7 @@
  specific language governing permissions and limitations
  under the License.
  */
-package org.apache.plc4x.java.amsads.connection;
-
-import org.apache.plc4x.java.base.connection.ChannelFactory;
-import org.apache.plc4x.java.base.connection.NettyPlcConnection;
-
-public abstract class BaseAMSADSConnection extends NettyPlcConnection {
-
-    public BaseAMSADSConnection(ChannelFactory channelFactory) {
-        super(channelFactory);
-    }
-
-    public BaseAMSADSConnection(ChannelFactory channelFactory, boolean awaitSessionSetupComplete) {
-        super(channelFactory, awaitSessionSetupComplete);
-    }
-
-}
+/**
+ * This package contains plc4x specific modeling.
+ */
+package org.apache.plc4x.java.amsads.model;
\ No newline at end of file
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/AMSADSProtocol.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/AMSADSProtocol.java
deleted file mode 100644
index b8c0e36..0000000
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/AMSADSProtocol.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- 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 org.apache.plc4x.java.df1.protocol;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.ByteBufUtil;
-import io.netty.channel.ChannelHandlerContext;
-import org.apache.plc4x.java.api.exceptions.PlcProtocolException;
-import org.apache.plc4x.java.base.PlcByteToMessageCodec;
-import org.apache.plc4x.java.df1.readwrite.DF1Command;
-import org.apache.plc4x.java.df1.readwrite.DF1Symbol;
-import org.apache.plc4x.java.df1.readwrite.DF1SymbolMessageFrame;
-import org.apache.plc4x.java.df1.readwrite.io.DF1SymbolIO;
-import org.apache.plc4x.java.utils.ReadBuffer;
-import org.apache.plc4x.java.utils.WriteBuffer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.List;
-
-public class AMSADSProtocol extends PlcByteToMessageCodec<AMSADSCommand> {
-
-    private static final Logger logger = LoggerFactory.getLogger(Df1Protocol.class);
-
-    private final short localAddr;
-    private final short remoteAddr;
-    private final DF1SymbolIO df1SymbolIO;
-
-    public AMSADSProtocol(short localAddr, short remoteAddr) {
-        this.localAddr = localAddr;
-        this.remoteAddr = remoteAddr;
-        df1SymbolIO = new DF1SymbolIO();
-    }
-
-    @Override
-    public void channelActive(ChannelHandlerContext ctx) throws Exception {
-    }
-
-    @Override
-    protected void encode(ChannelHandlerContext ctx, DF1Command msg, ByteBuf out) throws Exception {
-        // Create a new df1 frame for transmitting the command
-        DF1SymbolMessageFrame frame = new DF1SymbolMessageFrame(remoteAddr, localAddr, msg);
-
-        // Serialize the message
-        WriteBuffer writeBuffer = new WriteBuffer(frame.getLengthInBytes(), false);
-        df1SymbolIO.serialize(writeBuffer, frame);
-        byte[] data = writeBuffer.getData();
-
-        // Send the serialized data
-        // ctx.writeAndFlush(data);
-        out.writeBytes(data);
-    }
-
-    @Override
-    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
-        DF1Symbol resp;
-
-//        do {
-
-            in.markReaderIndex();
-
-            short size = 0x00;
-
-            // Yes, it's a little complicated, but we need to find out if we've got enough data.
-            if (in.readableBytes() > 1) {
-                if (in.getUnsignedByte(0) != (short) 0x10) {
-                    logger.warn("Expecting DF1 magic number: {}", 0x10);
-                    if (logger.isDebugEnabled()) {
-                        logger.debug("Got Data: {}", ByteBufUtil.hexDump(in));
-                    }
-                    exceptionCaught(ctx, new PlcProtocolException(
-                        String.format("Expecting DF1 magic number: %02X", 0x10)));
-                    return;
-                }
-
-                short symbolType = in.getUnsignedByte(1);
-                switch (symbolType) {
-                    case (short) 0x02: {
-                        if (in.readableBytes() < 5) {
-                            return;
-                        }
-                        short commandType = in.getUnsignedByte(4);
-                        switch (commandType) {
-                            case (short) 0x01: {
-                                if (in.readableBytes() < 11) {
-                                    return;
-                                }
-                                break;
-                            }
-                            case (short) 0x41: {
-                            /*int transactionCounter = in.getUnsignedShort(6);
-                            if(!readRequestSizes.containsKey(transactionCounter)) {
-                                logger.warn("Unknown transaction counter: {}", transactionCounter);
-                                if (logger.isDebugEnabled()) {
-                                    logger.debug("Got Data: {}", ByteBufUtil.hexDump(in));
-                                }
-                                exceptionCaught(ctx, new PlcProtocolException(
-                                    String.format("Unknown transaction counte: %04X", transactionCounter)));
-                                return;
-                            }
-                            size = readRequestSizes.remove(transactionCounter);
-                            if(in.readableBytes() < 8 + size) {
-                                return;
-                            }*/
-                                // TODO: Let's just assume all is good for now ...
-                                break;
-                            }
-                        }
-                        break;
-                    }
-                    case (short) 0x03: {
-                        if (in.readableBytes() < 4) {
-                            return;
-                        }
-                        break;
-                    }
-                }
-            }
-
-            // Parse the message received from the DF1 device
-            byte[] data = new byte[in.readableBytes()];
-            in.readBytes(data);
-            ReadBuffer readBuffer = new ReadBuffer(data, false);
-
-            resp = df1SymbolIO.parse(readBuffer);
-
-//        } while (readWasSucessfull);
-//        // TODO if unableto read
-//        in.resetReaderIndex();
-
-        // Add the received message to the output
-        out.add(resp);
-    }
-
-    @Override
-    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
-        cause.printStackTrace();
-        ctx.close();
-        //super.exceptionCaught(ctx, cause);
-    }
-}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Ads2PayloadProtocol.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Ads2PayloadProtocol.java
new file mode 100644
index 0000000..3e29980
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Ads2PayloadProtocol.java
@@ -0,0 +1,387 @@
+/*
+ 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 org.apache.plc4x.java.amsads.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageCodec;
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.configuration2.SystemConfiguration;
+import org.apache.plc4x.java.amsads.protocol.exception.AdsProtocolOverflowException;
+import org.apache.plc4x.java.amsads.readwrite.AmsHeader;
+import org.apache.plc4x.java.amsads.readwrite.AmsNetId;
+import org.apache.plc4x.java.amsads.readwrite.AmsPacket;
+import org.apache.plc4x.java.amsads.readwrite.State;
+import org.apache.plc4x.java.amsads.readwrite.types.CommandId;
+import org.graalvm.compiler.nodes.Invoke;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class Ads2PayloadProtocol extends MessageToMessageCodec<ByteBuf, AmsPacket> {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Ads2PayloadProtocol.class);
+
+    private static final Configuration CONF = new SystemConfiguration();
+    private static final long MAX_NUM_STAMPS = CONF.getLong("plc4x.ads2payloadprotocol.max_num_stamps", 512L);
+    private static final long MAX_NUM_SAMPLES = CONF.getLong("plc4x.ads2payloadprotocol.max_num_samples", 1024L);
+    private static final long ADS_READ_COMMAND_MAX_BYTES = CONF.getLong("plc4x.ads2payloadprotocol.ads_read_command_max_bytes", 134217728L);
+    private static final long ADS_WRITE_COMMAND_MAX_BYTES = CONF.getLong("plc4x.ads2payloadprotocol.ads_write_command_max_bytes", 134217728L);
+    private static final long ADS_WRITE_CONTROL_COMMAND_MAX_BYTES = CONF.getLong("plc4x.ads2payloadprotocol.ads_write_control_command_max_bytes", 134217728L);
+    private static final long ADS_NOTIFICATION_SAMPLE_MAX_BYTES = CONF.getLong("plc4x.ads2payloadprotocol.ads_notification_sample_max_bytes", 134217728L);
+    private static final long ADS_READ_WRITE_COMMAND_REQUEST_MAX_BYTES = CONF.getLong("plc4x.ads2payloadprotocol.ads_read_write_command_request_max_bytes", 134217728L);
+    private static final long ADS_READ_WRITE_COMMAND_RESPONSE_MAX_BYTES = CONF.getLong("plc4x.ads2payloadprotocol.ads_read_write_command_response_max_bytes", 134217728L);
+
+    private final ConcurrentMap<Long, AmsPacket> requests;
+
+    public Ads2PayloadProtocol() {
+        this.requests = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * Resets this protocol and discard all send requests.
+     */
+    public void reset() {
+        requests.clear();
+    }
+
+    @Override
+    protected void encode(ChannelHandlerContext channelHandlerContext, AmsPacket amsPacket, List<Object> out) {
+        LOGGER.trace("(<--OUT): {}, {}, {}", channelHandlerContext, amsPacket, out);
+        Long invokeId = amsPacket.getAmsHeader().getInvokeId();
+        if (invokeId != 0L) {
+            requests.put(invokeId, amsPacket);
+        }
+        out.add(amsPacket.getByteBuf());
+    }
+
+    @Override
+    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> out) {
+        if (byteBuf == Unpooled.EMPTY_BUFFER) {
+            // Cleanup...
+            reset();
+            return;
+        }
+        LOGGER.trace("(-->IN): {}, {}, {}", channelHandlerContext, byteBuf, out);
+        AmsNetId targetAmsNetId = AmsNetId.of(byteBuf);
+        AmsPort targetAmsPort = AmsPort.of(byteBuf);
+        AmsNetId sourceAmsNetId = AmsNetId.of(byteBuf);
+        AmsPort sourceAmsPort = AmsPort.of(byteBuf);
+        CommandId commandId = CommandId.of(byteBuf);
+        State stateId = State.of(byteBuf);
+        DataLength dataLength = DataLength.of(byteBuf);
+        AmsError errorCode = AmsError.of(byteBuf);
+        Invoke invoke = Invoke.of(byteBuf);
+        AmsPacket correlatedAmsPacket = requests.remove(invoke);
+        if (correlatedAmsPacket != null) {
+            LOGGER.debug("Correlated packet received {}", correlatedAmsPacket);
+        }
+        if (dataLength.getAsLong() > Integer.MAX_VALUE) {
+            throw new AdsProtocolOverflowException(Integer.class, dataLength.getAsLong());
+        }
+        ByteBuf commandBuffer = byteBuf.readBytes((int) dataLength.getAsLong());
+        AmsHeader amsHeader = AmsHeader.of(targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort, commandId, stateId, dataLength, errorCode, invoke);
+        final AmsPacket amsPacket;
+        switch (commandId) {
+            case INVALID:
+                amsPacket = handleInvalidCommand(commandBuffer, amsHeader);
+                break;
+            case ADS_READ_DEVICE_INFO:
+                amsPacket = handleADSReadDeviceInfoCommand(stateId, commandBuffer, amsHeader);
+                break;
+            case ADS_READ:
+                amsPacket = handleADSReadCommand(stateId, commandBuffer, amsHeader);
+                break;
+            case ADS_WRITE:
+                amsPacket = handleADSWriteCommand(stateId, commandBuffer, amsHeader);
+                break;
+            case ADS_READ_STATE:
+                amsPacket = handleADSReadStateCommand(stateId, commandBuffer, amsHeader);
+                break;
+            case ADS_WRITE_CONTROL:
+                amsPacket = handleADSWriteControlCommand(stateId, commandBuffer, amsHeader);
+                break;
+            case ADS_ADD_DEVICE_NOTIFICATION:
+                amsPacket = handleADSAddDeviceNotificationCommand(stateId, commandBuffer, amsHeader);
+                break;
+            case ADS_DELETE_DEVICE_NOTIFICATION:
+                amsPacket = handADSDeleteDeviceNotificationCommand(stateId, commandBuffer, amsHeader);
+                break;
+            case ADS_DEVICE_NOTIFICATION:
+                amsPacket = handleADSDeviceNotificationCommand(stateId, commandBuffer, amsHeader);
+                break;
+            case ADS_READ_WRITE:
+                amsPacket = handleADSReadWriteCommand(stateId, commandBuffer, amsHeader);
+                break;
+            case UNKNOWN:
+            default:
+                amsPacket = handleUnknownCommand(commandBuffer, amsHeader);
+        }
+        LOGGER.debug("Received amsPacket {}", amsPacket);
+        out.add(amsPacket);
+        if (commandBuffer.readableBytes() > 0) {
+            throw new IllegalStateException("Unread bytes left: " + commandBuffer.readableBytes());
+        }
+    }
+
+
+    private AmsPacket handleInvalidCommand(ByteBuf commandBuffer, AmsHeader amsHeader) {
+        return UnknownCommand.of(amsHeader, commandBuffer);
+    }
+
+    private AmsPacket handleADSReadDeviceInfoCommand(State stateId, ByteBuf commandBuffer, AmsHeader amsHeader) {
+        AmsPacket amsPacket;
+        if (stateId.isRequest()) {
+            amsPacket = AdsReadDeviceInfoRequest.of(amsHeader);
+        } else {
+            Result result = Result.of(commandBuffer);
+            MajorVersion majorVersion = MajorVersion.of(commandBuffer);
+            MinorVersion minorVersion = MinorVersion.of(commandBuffer);
+            Version version = Version.of(commandBuffer);
+            Device device = Device.of(commandBuffer);
+            amsPacket = AdsReadDeviceInfoResponse.of(amsHeader, result, majorVersion, minorVersion, version, device);
+        }
+        return amsPacket;
+    }
+
+    private AmsPacket handleADSReadCommand(State stateId, ByteBuf commandBuffer, AmsHeader amsHeader) {
+        AmsPacket amsPacket;
+        if (stateId.isRequest()) {
+            IndexGroup indexGroup = IndexGroup.of(commandBuffer);
+            IndexOffset indexOffset = IndexOffset.of(commandBuffer);
+            Length length = Length.of(commandBuffer);
+            amsPacket = AdsReadRequest.of(amsHeader, indexGroup, indexOffset, length);
+        } else {
+            Result result = Result.of(commandBuffer);
+            Length length = Length.of(commandBuffer);
+            if (length.getAsLong() > Integer.MAX_VALUE) {
+                throw new AdsProtocolOverflowException(Integer.class, length.getAsLong());
+            }
+            if (length.getAsLong() > ADS_READ_COMMAND_MAX_BYTES) {
+                throw new AdsProtocolOverflowException("ADS_READ_COMMAND_MAX_BYTES", ADS_READ_COMMAND_MAX_BYTES, length.getAsLong());
+            }
+            byte[] dataToRead = new byte[(int) length.getAsLong()];
+            commandBuffer.readBytes(dataToRead);
+            Data data = Data.of(dataToRead);
+            amsPacket = AdsReadResponse.of(amsHeader, result, length, data);
+        }
+        return amsPacket;
+    }
+
+    private AmsPacket handleADSWriteCommand(State stateId, ByteBuf commandBuffer, AmsHeader amsHeader) {
+        AmsPacket amsPacket;
+        if (stateId.isRequest()) {
+            IndexGroup indexGroup = IndexGroup.of(commandBuffer);
+            IndexOffset indexOffset = IndexOffset.of(commandBuffer);
+            Length length = Length.of(commandBuffer);
+            if (length.getAsLong() > Integer.MAX_VALUE) {
+                throw new AdsProtocolOverflowException(Integer.class, length.getAsLong());
+            }
+            if (length.getAsLong() > ADS_WRITE_COMMAND_MAX_BYTES) {
+                throw new AdsProtocolOverflowException("ADS_WRITE_COMMAND_MAX_BYTES", ADS_WRITE_COMMAND_MAX_BYTES, length.getAsLong());
+            }
+            byte[] dataToRead = new byte[(int) length.getAsLong()];
+            commandBuffer.readBytes(dataToRead);
+            Data data = Data.of(dataToRead);
+            amsPacket = AdsWriteRequest.of(amsHeader, indexGroup, indexOffset, length, data);
+        } else {
+            Result result = Result.of(commandBuffer);
+            amsPacket = AdsWriteResponse.of(amsHeader, result);
+        }
+        return amsPacket;
+    }
+
+
+    private AmsPacket handleADSReadStateCommand(State stateId, ByteBuf commandBuffer, AmsHeader amsHeader) {
+        AmsPacket amsPacket;
+        if (stateId.isRequest()) {
+            amsPacket = AdsReadStateRequest.of(amsHeader);
+        } else {
+            Result result = Result.of(commandBuffer);
+            AdsState adsState = AdsState.of(commandBuffer);
+            DeviceState deviceState = DeviceState.of(commandBuffer);
+            amsPacket = AdsReadStateResponse.of(amsHeader, result, adsState, deviceState);
+        }
+        return amsPacket;
+    }
+
+    private AmsPacket handleADSWriteControlCommand(State stateId, ByteBuf commandBuffer, AmsHeader amsHeader) {
+        AmsPacket amsPacket;
+        if (stateId.isRequest()) {
+            AdsState adsState = AdsState.of(commandBuffer);
+            DeviceState deviceState = DeviceState.of(commandBuffer);
+            Length length = Length.of(commandBuffer);
+            if (length.getAsLong() > Integer.MAX_VALUE) {
+                throw new AdsProtocolOverflowException(Integer.class, length.getAsLong());
+            }
+            if (length.getAsLong() > ADS_WRITE_CONTROL_COMMAND_MAX_BYTES) {
+                throw new AdsProtocolOverflowException("ADS_WRITE_CONTROL_COMMAND_MAX_BYTES", ADS_WRITE_CONTROL_COMMAND_MAX_BYTES, length.getAsLong());
+            }
+            byte[] dataToRead = new byte[(int) length.getAsLong()];
+            commandBuffer.readBytes(dataToRead);
+            Data data = Data.of(dataToRead);
+            amsPacket = AdsWriteControlRequest.of(amsHeader, adsState, deviceState, length, data);
+        } else {
+            Result result = Result.of(commandBuffer);
+            amsPacket = AdsWriteControlResponse.of(amsHeader, result);
+        }
+        return amsPacket;
+    }
+
+    private AmsPacket handleADSAddDeviceNotificationCommand(State stateId, ByteBuf commandBuffer, AmsHeader amsHeader) {
+        AmsPacket amsPacket;
+        if (stateId.isRequest()) {
+            IndexGroup indexGroup = IndexGroup.of(commandBuffer);
+            IndexOffset indexOffset = IndexOffset.of(commandBuffer);
+            Length length = Length.of(commandBuffer);
+            TransmissionMode transmissionMode = TransmissionMode.of(commandBuffer);
+            MaxDelay maxDelay = MaxDelay.of(commandBuffer);
+            CycleTime cycleTime = CycleTime.of(commandBuffer);
+            commandBuffer.skipBytes(AdsAddDeviceNotificationRequest.Reserved.NUM_BYTES);
+            amsPacket = AdsAddDeviceNotificationRequest.of(amsHeader, indexGroup, indexOffset, length, transmissionMode, maxDelay, cycleTime);
+        } else {
+            Result result = Result.of(commandBuffer);
+            NotificationHandle notificationHandle = NotificationHandle.of(commandBuffer);
+            amsPacket = AdsAddDeviceNotificationResponse.of(amsHeader, result, notificationHandle);
+        }
+        return amsPacket;
+    }
+
+    private AmsPacket handADSDeleteDeviceNotificationCommand(State stateId, ByteBuf commandBuffer, AmsHeader amsHeader) {
+        AmsPacket amsPacket;
+        if (stateId.isRequest()) {
+            NotificationHandle notificationHandle = NotificationHandle.of(commandBuffer);
+            amsPacket = AdsDeleteDeviceNotificationRequest.of(amsHeader, notificationHandle);
+        } else {
+            Result result = Result.of(commandBuffer);
+            amsPacket = AdsDeleteDeviceNotificationResponse.of(amsHeader, result);
+        }
+        return amsPacket;
+    }
+
+    private AmsPacket handleADSDeviceNotificationCommand(State stateId, ByteBuf commandBuffer, AmsHeader amsHeader) {
+        AmsPacket amsPacket;
+        if (stateId.isRequest()) {
+            Length length = Length.of(commandBuffer);
+            if (length.getAsLong() > Integer.MAX_VALUE) {
+                throw new AdsProtocolOverflowException(Integer.class, length.getAsLong());
+            }
+            Stamps stamps = Stamps.of(commandBuffer);
+            if (stamps.getAsLong() > Integer.MAX_VALUE) {
+                throw new AdsProtocolOverflowException(Integer.class, stamps.getAsLong());
+            }
+            // Note: the length includes the 4 Bytes of stamps which we read already so we substract.
+            ByteBuf adsDeviceNotificationBuffer = commandBuffer.readBytes((int) length.getAsLong() - Stamps.NUM_BYTES);
+            List<AdsStampHeader> adsStampHeaders = new ArrayList<>((int) stamps.getAsLong());
+            if (stamps.getAsLong() > MAX_NUM_STAMPS) {
+                throw new AdsProtocolOverflowException("MAX_NUM_STAMPS", MAX_NUM_STAMPS, length.getAsLong());
+            }
+            for (int i = 1; i <= stamps.getAsLong(); i++) {
+                AdsStampHeader adsStampHeader = handleStampHeader(adsDeviceNotificationBuffer);
+                adsStampHeaders.add(adsStampHeader);
+            }
+            amsPacket = AdsDeviceNotificationRequest.of(amsHeader, length, stamps, adsStampHeaders);
+        } else {
+            amsPacket = UnknownCommand.of(amsHeader, commandBuffer);
+        }
+        return amsPacket;
+    }
+
+    private AdsStampHeader handleStampHeader(ByteBuf adsDeviceNotificationBuffer) {
+        TimeStamp timeStamp = TimeStamp.of(adsDeviceNotificationBuffer);
+        Samples samples = Samples.of(adsDeviceNotificationBuffer);
+
+        List<AdsNotificationSample> adsNotificationSamples = new LinkedList<>();
+        if (samples.getAsLong() > MAX_NUM_SAMPLES) {
+            throw new AdsProtocolOverflowException("MAX_NUM_SAMPLES", MAX_NUM_SAMPLES, samples.getAsLong());
+        }
+        for (int i = 1; i <= samples.getAsLong(); i++) {
+            AdsNotificationSample adsNotificationSample = handleAdsNotificartionSample(adsDeviceNotificationBuffer);
+            adsNotificationSamples.add(adsNotificationSample);
+
+        }
+        return AdsStampHeader.of(timeStamp, samples, adsNotificationSamples);
+    }
+
+    private AdsNotificationSample handleAdsNotificartionSample(ByteBuf adsDeviceNotificationBuffer) {
+        NotificationHandle notificationHandle = NotificationHandle.of(adsDeviceNotificationBuffer);
+        SampleSize sampleSize = SampleSize.of(adsDeviceNotificationBuffer);
+        if (sampleSize.getAsLong() > Integer.MAX_VALUE) {
+            throw new AdsProtocolOverflowException(Integer.class, sampleSize.getAsLong());
+        }
+        if (sampleSize.getAsLong() > ADS_NOTIFICATION_SAMPLE_MAX_BYTES) {
+            throw new AdsProtocolOverflowException("ADS_NOTIFICATION_SAMPLE_MAX_BYTES", ADS_NOTIFICATION_SAMPLE_MAX_BYTES, sampleSize.getAsLong());
+        }
+        // TODO: do we need a special marker class for: Notice: If your handle becomes invalid, one notification without data will be send once as advice.
+        byte[] dataToRead = new byte[(int) sampleSize.getAsLong()];
+        adsDeviceNotificationBuffer.readBytes(dataToRead);
+        Data data = Data.of(dataToRead);
+        return AdsNotificationSample.of(notificationHandle, sampleSize, data);
+    }
+
+    private AmsPacket handleADSReadWriteCommand(State stateId, ByteBuf commandBuffer, AmsHeader amsHeader) {
+        AmsPacket amsPacket;
+        if (stateId.isRequest()) {
+            IndexGroup indexGroup = IndexGroup.of(commandBuffer);
+            IndexOffset indexOffset = IndexOffset.of(commandBuffer);
+            ReadLength readLength = ReadLength.of(commandBuffer);
+            if (readLength.getAsLong() > Integer.MAX_VALUE) {
+                throw new AdsProtocolOverflowException(Integer.class, readLength.getAsLong());
+            }
+            WriteLength writeLength = WriteLength.of(commandBuffer);
+            if (writeLength.getAsLong() > Integer.MAX_VALUE) {
+                throw new AdsProtocolOverflowException(Integer.class, writeLength.getAsLong());
+            }
+            if (readLength.getAsLong() + writeLength.getAsLong() > Integer.MAX_VALUE) {
+                throw new AdsProtocolOverflowException(Integer.class, readLength.getAsLong() + writeLength.getAsLong());
+            }
+            if (readLength.getAsLong() > ADS_READ_WRITE_COMMAND_REQUEST_MAX_BYTES) {
+                throw new AdsProtocolOverflowException("ADS_READ_WRITE_COMMAND_REQUEST_MAX_BYTES", ADS_READ_WRITE_COMMAND_REQUEST_MAX_BYTES, readLength.getAsLong());
+            }
+            byte[] dataToRead = new byte[(int) readLength.getAsLong()];
+            commandBuffer.readBytes(dataToRead);
+            Data data = Data.of(dataToRead);
+            amsPacket = AdsReadWriteRequest.of(amsHeader, indexGroup, indexOffset, readLength, writeLength, data);
+        } else {
+            Result result = Result.of(commandBuffer);
+            Length length = Length.of(commandBuffer);
+            if (length.getAsLong() > Integer.MAX_VALUE) {
+                throw new AdsProtocolOverflowException(Integer.class, length.getAsLong());
+            }
+            if (length.getAsLong() > ADS_READ_WRITE_COMMAND_RESPONSE_MAX_BYTES) {
+                throw new AdsProtocolOverflowException("ADS_READ_WRITE_COMMAND_RESPONSE_MAX_BYTES", ADS_READ_WRITE_COMMAND_RESPONSE_MAX_BYTES, length.getAsLong());
+            }
+            byte[] dataToRead = new byte[(int) length.getAsLong()];
+            commandBuffer.readBytes(dataToRead);
+            Data data = Data.of(dataToRead);
+            amsPacket = AdsReadWriteResponse.of(amsHeader, result, length, data);
+        }
+        return amsPacket;
+    }
+
+    private AmsPacket handleUnknownCommand(ByteBuf commandBuffer, AmsHeader amsHeader) {
+        return UnknownCommand.of(amsHeader, commandBuffer);
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Payload2SerialProtocol.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Payload2SerialProtocol.java
new file mode 100644
index 0000000..b0a5f4e
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Payload2SerialProtocol.java
@@ -0,0 +1,171 @@
+/*
+ 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 org.apache.plc4x.java.amsads.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageCodec;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.ScheduledFuture;
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.apache.plc4x.java.api.exceptions.PlcProtocolException;
+import org.apache.plc4x.java.api.exceptions.PlcProtocolPayloadTooBigException;
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class Payload2SerialProtocol extends MessageToMessageCodec<ByteBuf, ByteBuf> {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Payload2SerialProtocol.class);
+
+    private final AtomicInteger fragmentCounter = new AtomicInteger(0);
+
+    private AtomicReference<ScheduledFuture<?>> currentRetryer = new AtomicReference<>();
+
+    @Override
+    protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf amsPacket, List<Object> out) throws PlcProtocolPayloadTooBigException {
+        if (amsPacket == Unpooled.EMPTY_BUFFER) {
+            // Cleanup...
+            ScheduledFuture<?> scheduledFuture = currentRetryer.get();
+            if (scheduledFuture != null) {
+                scheduledFuture.cancel(true);
+            }
+            return;
+        }
+        LOGGER.trace("(<--OUT): {}, {}, {}", channelHandlerContext, amsPacket, out);
+        int fragmentNumber = fragmentCounter.getAndUpdate(value -> value > 255 ? 0 : ++value);
+        LOGGER.debug("Using fragmentNumber {} for {}", fragmentNumber, amsPacket);
+        UserData userData = UserData.of(amsPacket);
+        if (userData.getCalculatedLength() > 255) {
+            throw new PlcProtocolPayloadTooBigException("ADS/AMS", 255, (int) userData.getCalculatedLength(), amsPacket);
+        }
+        AmsSerialFrame amsSerialFrame = AmsSerialFrame.of(FragmentNumber.of((byte) fragmentNumber), userData);
+
+        MutableInt retryCount = new MutableInt(0);
+        ScheduledFuture<?> oldRetryer = currentRetryer.get();
+        if (oldRetryer != null) {
+            oldRetryer.cancel(false);
+        }
+        currentRetryer.set(channelHandlerContext.executor().scheduleAtFixedRate(() -> {
+            LOGGER.trace("Retrying {} the {} time", amsSerialFrame, retryCount);
+            int currentTry = retryCount.incrementAndGet();
+            if (currentTry > 10) {
+                // TODO: we might need to throw an exception to potentially cancel upstream waiting
+                channelHandlerContext.writeAndFlush(AmsSerialResetFrame.of(FragmentNumber.of((byte) fragmentNumber)));
+                PlcRuntimeException plcRuntimeException = new PlcRuntimeException("Retry exhausted after " + retryCount + " times");
+                channelHandlerContext.fireExceptionCaught(plcRuntimeException);
+                throw plcRuntimeException;
+            } else {
+                channelHandlerContext.writeAndFlush(amsSerialFrame);
+            }
+        }, 100, 100, TimeUnit.MILLISECONDS));
+        out.add(amsSerialFrame.getByteBuf());
+    }
+
+    @Override
+    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> out) throws Exception {
+        LOGGER.trace("(-->IN): {}, {}, {}", channelHandlerContext, byteBuf, out);
+        if (byteBuf.readableBytes() < MagicCookie.NUM_BYTES + TransmitterAddress.NUM_BYTES + ReceiverAddress.NUM_BYTES + FragmentNumber.NUM_BYTES) {
+            return;
+        }
+        MagicCookie magicCookie = MagicCookie.of(byteBuf);
+        TransmitterAddress transmitterAddress = TransmitterAddress.of(byteBuf);
+        ReceiverAddress receiverAddress = ReceiverAddress.of(byteBuf);
+        FragmentNumber fragmentNumber = FragmentNumber.of(byteBuf);
+        int expectedFrameNumber = fragmentCounter.get() - 1;
+        if (expectedFrameNumber < 0) {
+            expectedFrameNumber = 255;
+        }
+        if (fragmentNumber.getAsByte() != expectedFrameNumber) {
+            LOGGER.warn("Unexpected fragment {} received. Expected {}", fragmentNumber, expectedFrameNumber);
+        }
+        UserDataLength userDataLength = UserDataLength.of(byteBuf);
+        UserData userData;
+        byte userDataLengthAsByte = userDataLength.getAsByte();
+        if (byteBuf.readableBytes() < userDataLengthAsByte) {
+            return;
+        }
+        if (userDataLengthAsByte > 0) {
+            byte[] userDataByteArray = new byte[userDataLengthAsByte];
+            byteBuf.readBytes(userDataByteArray);
+            userData = UserData.of(userDataByteArray);
+        } else {
+            userData = UserData.EMPTY;
+        }
+        CRC crc = CRC.of(byteBuf);
+
+        // we don't need to retransmit
+        ScheduledFuture<?> scheduledFuture = currentRetryer.get();
+        if (scheduledFuture != null) {
+            scheduledFuture.cancel(false);
+        }
+
+        Runnable postAction = null;
+        switch (magicCookie.getAsInt()) {
+            case AmsSerialFrame.ID:
+                AmsSerialFrame amsSerialFrame = AmsSerialFrame.of(magicCookie, transmitterAddress, receiverAddress, fragmentNumber, userDataLength, userData, crc);
+                LOGGER.debug("Ams Serial Frame received {}", amsSerialFrame);
+                postAction = () -> {
+                    // TODO: check if this is the right way to ack a package.
+                    ChannelFuture channelFuture = channelHandlerContext.writeAndFlush(AmsSerialAcknowledgeFrame.of(transmitterAddress, receiverAddress, fragmentNumber).getByteBuf());
+                    // waiting for the ack-frame to be transmitted before we forward the package
+                    try {
+                        channelFuture.await();
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        throw new PlcRuntimeException(e);
+                    }
+                    out.add(userData.getByteBuf());
+                };
+                break;
+            case AmsSerialAcknowledgeFrame.ID:
+                AmsSerialAcknowledgeFrame amsSerialAcknowledgeFrame = AmsSerialAcknowledgeFrame.of(magicCookie, transmitterAddress, receiverAddress, fragmentNumber, userDataLength, crc);
+                LOGGER.debug("Ams Serial ACK Frame received {}", amsSerialAcknowledgeFrame);
+                ReferenceCountUtil.release(byteBuf);
+                break;
+            case AmsSerialResetFrame.ID:
+                // TODO: how to react to a reset
+                AmsSerialResetFrame amsSerialResetFrame = AmsSerialResetFrame.of(magicCookie, transmitterAddress, receiverAddress, fragmentNumber, userDataLength, crc);
+                LOGGER.debug("Ams Serial Reset Frame received {}", amsSerialResetFrame);
+                ReferenceCountUtil.release(byteBuf);
+                break;
+            default:
+                throw new PlcProtocolException("Unknown type: " + magicCookie);
+        }
+        CRC calculatedCrc = CRC.of(DigestUtil.calculateCrc16(magicCookie, transmitterAddress, receiverAddress, fragmentNumber, userDataLength, userData));
+        if (!crc.equals(calculatedCrc)) {
+            throw new PlcProtocolException("CRC checksum wrong. Got " + crc + " expected " + calculatedCrc);
+        }
+
+        if (postAction != null) {
+            postAction.run();
+        }
+
+        if (byteBuf.readableBytes() > 0) {
+            throw new IllegalStateException("Unread bytes left: " + byteBuf.readableBytes());
+        }
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Payload2TcpProtocol.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Payload2TcpProtocol.java
new file mode 100644
index 0000000..780cc6f
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Payload2TcpProtocol.java
@@ -0,0 +1,67 @@
+/*
+ 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 org.apache.plc4x.java.amsads.protocol;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageCodec;
+import org.apache.plc4x.java.amsads.readwrite.AmsTCPPacket;
+import org.apache.plc4x.java.amsads.readwrite.AmsTcpHeader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class Payload2TcpProtocol extends MessageToMessageCodec<ByteBuf, ByteBuf> {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Payload2TcpProtocol.class);
+
+    @Override
+    protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf amsPacket, List<Object> out) {
+        LOGGER.trace("(<--OUT): {}, {}, {}", channelHandlerContext, amsPacket, out);
+        out.add(AmsTCPPacket.of(UserData.of(amsPacket)).getByteBuf());
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> out) {
+        if (byteBuf == Unpooled.EMPTY_BUFFER) {
+            // Cleanup...
+            return;
+        }
+        LOGGER.trace("(-->IN): {}, {}, {}", channelHandlerContext, byteBuf, out);
+        if (byteBuf.readableBytes() < AmsTcpHeader.Reserved.NUM_BYTES + TcpLength.NUM_BYTES) {
+            // wait till we can read the length header
+            return;
+        }
+        if (byteBuf.readableBytes() < byteBuf.getUnsignedIntLE(AmsTcpHeader.Reserved.NUM_BYTES)) {
+            // wait till we have a complete ADS packet
+            return;
+        }
+        // Reserved
+        byteBuf.skipBytes(AmsTcpHeader.Reserved.NUM_BYTES);
+        TcpLength packetLength = TcpLength.of(byteBuf);
+        AmsTcpHeader amsTcpHeader = AmsTcpHeader.of(packetLength);
+        LOGGER.debug("AMS TCP Header {}", amsTcpHeader);
+
+        out.add(byteBuf.readBytes((int) packetLength.getAsLong()));
+    }
+
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Plc4XDf1Protocol.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Plc4XDf1Protocol.java
deleted file mode 100644
index d169dca..0000000
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Plc4XDf1Protocol.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- 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 org.apache.plc4x.java.df1.protocol;
-
-import io.netty.channel.ChannelHandlerContext;
-import org.apache.commons.lang3.NotImplementedException;
-import org.apache.commons.lang3.tuple.Pair;
-import org.apache.plc4x.java.api.messages.PlcReadRequest;
-import org.apache.plc4x.java.api.messages.PlcRequest;
-import org.apache.plc4x.java.api.messages.PlcResponse;
-import org.apache.plc4x.java.api.messages.PlcWriteRequest;
-import org.apache.plc4x.java.api.model.PlcField;
-import org.apache.plc4x.java.api.types.PlcResponseCode;
-import org.apache.plc4x.java.base.PlcMessageToMessageCodec;
-import org.apache.plc4x.java.base.messages.DefaultPlcReadResponse;
-import org.apache.plc4x.java.base.messages.InternalPlcReadRequest;
-import org.apache.plc4x.java.base.messages.PlcRequestContainer;
-import org.apache.plc4x.java.base.messages.items.BaseDefaultFieldItem;
-import org.apache.plc4x.java.base.messages.items.DefaultIntegerFieldItem;
-import org.apache.plc4x.java.df1.*;
-import org.apache.plc4x.java.df1.readwrite.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicInteger;
-
-public class Plc4XDf1Protocol extends PlcMessageToMessageCodec<DF1Symbol, PlcRequestContainer> {
-
-    private static final Logger logger = LoggerFactory.getLogger(Plc4XDf1Protocol.class);
-
-    private final AtomicInteger transactionId = new AtomicInteger(1);
-    private Map<Integer, PlcRequestContainer> requests = new ConcurrentHashMap<>();
-
-    @Override
-    protected void encode(ChannelHandlerContext ctx, PlcRequestContainer msg, List<Object> out) throws Exception {
-        logger.trace("Received Request {} to send out", msg);
-        if (msg.getRequest() instanceof PlcReadRequest) {
-            for (PlcField field : ((PlcReadRequest) msg.getRequest()).getFields()) {
-                if (!(field instanceof Df1Field)) {
-                    throw new IllegalArgumentException("Invalid field type found inside Df1 Request");
-                }
-                int address = ((Df1Field) field).getAddress();
-                short size = ((Df1Field) field).getDataType().getLength();
-                int transactionId = this.transactionId.getAndIncrement();
-                while (((transactionId & 0xFF) == 0x10) || (((transactionId >>> 8) & 0xFF) == 0x10)) {
-                    // prevent that one of the transactionID bytes is 0x10, which has to be escaped by double 0x10 and makes life a lot harder
-                    transactionId = this.transactionId.getAndIncrement();
-                }
-                logger.debug("Creating request for offset {}, with length {} and transaction id {}", address, size, transactionId);
-
-                requests.put(transactionId, msg);
-
-                switch (((Df1Field) field).getAddress_type()) {
-                    case OFFSET:
-                        out.add(new DF1UnprotectedReadRequest((short) 0x00, (short)transactionId, address, size));
-                        break;
-                    case LOGICAL:
-                        // TODO: add 'protected typed logical read' to mspec
-                        throw new NotImplementedException("not yet implemented in mspec");
-                }
-
-            }
-        } else {
-            throw new IllegalStateException("This should not happen!");
-        }
-    }
-
-    @Override
-    protected void decode(ChannelHandlerContext ctx, DF1Symbol msg, List<Object> out) throws Exception {
-        logger.debug("Received DF1 Command incoming {}", msg);
-
-        if (msg instanceof DF1SymbolMessageFrameNAK) {
-            logger.warn("Received a response NAK, notify all requests");
-            for (Map.Entry<Integer, PlcRequestContainer> entry : requests.entrySet()) {
-                entry.getValue().getResponseFuture().complete(
-                    new DefaultPlcReadResponse(
-                        ((InternalPlcReadRequest) entry.getValue().getRequest()),
-                        Collections.singletonMap("erster", Pair.of(PlcResponseCode.INTERNAL_ERROR, new DefaultIntegerFieldItem(-1)))
-                    ));
-            }
-            return;
-        } else if (msg instanceof DF1SymbolMessageFrameACK) {
-            logger.warn("Received a response ACK :D");
-            return;
-        }
-
-        assert msg instanceof DF1SymbolMessageFrame;
-
-        DF1Command command = ((DF1SymbolMessageFrame) msg).getCommand();
-        int transactionId = command.getTransactionCounter();
-        if (!requests.containsKey(transactionId)) {
-            logger.warn("Received a response to unknown transaction id {}", transactionId);
-            ctx.fireExceptionCaught(new RuntimeException("Received a response to unknown transaction id"));
-            ctx.close();
-            return;
-        }
-        // As every response has a matching request, get this request based on the tpdu.
-        PlcRequestContainer requestContainer = requests.remove(transactionId);
-        PlcRequest request = requestContainer.getRequest();
-
-        // Handle the response.
-        PlcResponse response = null;
-        if (request instanceof PlcReadRequest) {
-            /*
-            Things to do
-            - check response code (if there is something like that?
-            - cast the bytes to right datatype
-            - create Response
-             */
-            // We can do this as we have only one fieldName in DF1
-            final String fieldName = ((PlcReadRequest) request).getFieldNames().iterator().next();
-            // TODO can there be another code than ok?
-            final PlcResponseCode responseCode = PlcResponseCode.OK;
-            // TODO maybe check for different status bytes
-            final Df1Field field = (Df1Field) ((PlcReadRequest) request).getField(fieldName);
-            // Cast byte and create response item
-            BaseDefaultFieldItem responseItem = null;
-            short[] data = ((DF1UnprotectedReadResponse)command).getData();
-            switch (field.getDataType()) {
-                case BIT:
-                    break;
-                case INTEGER:
-                    // TODO: type conversion is untested
-                    responseItem = new DefaultIntegerFieldItem((int)data[0] + ((int) data[1] << 8));
-                    break;
-                case FLOAT:
-                    break;
-                case BIT_STRING:
-                    break;
-                case ARRAY:
-                    break;
-                // TODO add all other cases here...
-                default:
-                    throw new NotImplementedException("The DataType " + field.getDataType() + " is currently not implemented!");
-            }
-            response = new DefaultPlcReadResponse(((InternalPlcReadRequest) request),
-                Collections.singletonMap(fieldName,
-                    Pair.of(responseCode, responseItem)));
-        } else if (request instanceof PlcWriteRequest) {
-            logger.warn("Writing is currently not implemented but received a write response?!");
-            ctx.close();
-            throw new NotImplementedException("This is currently not implemented!");
-        }
-
-        // Confirm the response being handled.
-        if (response != null) {
-            requestContainer.getResponseFuture().complete(response);
-        }
-    }
-
-
-}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Plc4x2AdsProtocol.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Plc4x2AdsProtocol.java
new file mode 100644
index 0000000..1664f40
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/Plc4x2AdsProtocol.java
@@ -0,0 +1,445 @@
+/*
+ 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 org.apache.plc4x.java.amsads.protocol;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageCodec;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.plc4x.java.amsads.model.AdsField;
+import org.apache.plc4x.java.amsads.model.DirectAdsField;
+import org.apache.plc4x.java.amsads.model.SymbolicAdsField;
+import org.apache.plc4x.java.amsads.protocol.exception.AdsException;
+import org.apache.plc4x.java.amsads.readwrite.*;
+import org.apache.plc4x.java.api.exceptions.PlcException;
+import org.apache.plc4x.java.api.exceptions.PlcIoException;
+import org.apache.plc4x.java.api.exceptions.PlcProtocolException;
+import org.apache.plc4x.java.api.exceptions.PlcProtocolPayloadTooBigException;
+import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.api.messages.PlcRequest;
+import org.apache.plc4x.java.api.messages.PlcWriteRequest;
+import org.apache.plc4x.java.api.model.PlcField;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.apache.plc4x.java.base.messages.*;
+import org.apache.plc4x.java.base.messages.items.BaseDefaultFieldItem;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import static org.apache.plc4x.java.amsads.protocol.util.LittleEndianDecoder.decodeData;
+import static org.apache.plc4x.java.amsads.protocol.util.LittleEndianEncoder.encodeData;
+
+public class Plc4x2AdsProtocol extends MessageToMessageCodec<AmsPacket, PlcRequestContainer<InternalPlcRequest, InternalPlcResponse>> {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Plc4x2AdsProtocol.class);
+
+    private static final AtomicLong correlationBuilder = new AtomicLong(1);
+
+    private final ConcurrentMap<Long, PlcRequestContainer<InternalPlcRequest, InternalPlcResponse>> requests;
+
+    private final ConcurrentMap<SymbolicAdsField, DirectAdsField> fieldMapping;
+
+    private List<Consumer<AdsDeviceNotificationRequest>> deviceNotificationListeners;
+
+    private final AmsNetId targetAmsNetId;
+    private final int targetAmsPort;
+    private final AmsNetId sourceAmsNetId;
+    private final int sourceAmsPort;
+
+    public Plc4x2AdsProtocol(AmsNetId targetAmsNetId, int targetAmsPort, AmsNetId sourceAmsNetId, int sourceAmsPort, ConcurrentMap<SymbolicAdsField, DirectAdsField> fieldMapping) {
+        this.targetAmsNetId = targetAmsNetId;
+        this.targetAmsPort = targetAmsPort;
+        this.sourceAmsNetId = sourceAmsNetId;
+        this.sourceAmsPort = sourceAmsPort;
+        this.requests = new ConcurrentHashMap<>();
+        this.fieldMapping = fieldMapping;
+        this.deviceNotificationListeners = new LinkedList<>();
+    }
+
+    @Override
+    protected void encode(ChannelHandlerContext ctx, PlcRequestContainer<InternalPlcRequest, InternalPlcResponse> msg, List<Object> out) throws Exception {
+        LOGGER.trace("(<--OUT): {}, {}, {}", ctx, msg, out);
+        PlcRequest request = msg.getRequest();
+        if (request instanceof PlcReadRequest) {
+            encodeReadRequest(msg, out);
+        } else if (request instanceof PlcWriteRequest) {
+            encodeWriteRequest(msg, out);
+        } else if (request instanceof PlcProprietaryRequest) {
+            encodeProprietaryRequest(msg, out);
+        } else {
+            throw new PlcProtocolException("Unknown type " + request.getClass());
+        }
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        LOGGER.trace("(-->ERR): {}", ctx, cause);
+        if (cause instanceof AdsException) {
+            Long invokeId = ((AdsException) cause).getInvokeId();
+            if (invokeId != null) {
+                PlcRequestContainer<InternalPlcRequest, InternalPlcResponse> remove = requests.remove(invokeId);
+                if (remove != null) {
+                    remove.getResponseFuture().completeExceptionally(new PlcIoException(cause));
+                } else {
+                    LOGGER.warn("Unrelated exception received {}", invokeId, cause);
+                }
+            } else {
+                super.exceptionCaught(ctx, cause);
+            }
+        } else if ((cause instanceof IOException) && (cause.getMessage().contains("Connection reset by peer") ||
+            cause.getMessage().contains("Operation timed out"))) {
+            String reason = cause.getMessage().contains("Connection reset by peer") ?
+                "Connection terminated unexpectedly" : "Remote host not responding";
+            if (!requests.isEmpty()) {
+                // If the connection is hung up, all still pending requests can be closed.
+                for (PlcRequestContainer requestContainer : requests.values()) {
+                    requestContainer.getResponseFuture().completeExceptionally(new PlcIoException(reason));
+                }
+                // Clear the list
+                requests.clear();
+            }
+        } else {
+            super.exceptionCaught(ctx, cause);
+        }
+    }
+
+    private void encodeWriteRequest(PlcRequestContainer<InternalPlcRequest, InternalPlcResponse> msg, List<Object> out) throws PlcException {
+        InternalPlcWriteRequest writeRequest = (InternalPlcWriteRequest) msg.getRequest();
+        if (writeRequest.getFields().size() != 1) {
+            throw new PlcProtocolException("Only one item supported");
+        }
+        PlcField field = writeRequest.getFields().get(0);
+        if (field instanceof SymbolicAdsField) {
+            DirectAdsField mappedField = fieldMapping.get(field);
+            LOGGER.debug("Replacing {} with {}", field, mappedField);
+            field = mappedField;
+        }
+        if (!(field instanceof DirectAdsField)) {
+            throw new PlcProtocolException("PlcField not of type DirectAdsField: " + field.getClass());
+        }
+        DirectAdsField directAdsField = (DirectAdsField) field;
+        Long invokeId = Invoke.of(correlationBuilder.incrementAndGet());
+        IndexGroup indexGroup = IndexGroup.of(directAdsField.getIndexGroup());
+        IndexOffset indexOffset = IndexOffset.of(directAdsField.getIndexOffset());
+
+        BaseDefaultFieldItem fieldItem = writeRequest.getFieldItems().get(0);
+        Object[] values = fieldItem.getValues();
+
+        byte[] bytes = encodeData(directAdsField.getAdsDataType(), values);
+        int bytesToBeWritten = bytes.length;
+        int maxTheoreticalSize = directAdsField.getAdsDataType().getTargetByteSize() * directAdsField.getNumberOfElements();
+        if (bytesToBeWritten > maxTheoreticalSize) {
+            LOGGER.debug("Requested AdsDatatype {} is exceeded by number of bytes {}. Limit {}.", directAdsField.getAdsDataType(), bytesToBeWritten, maxTheoreticalSize);
+            throw new PlcProtocolPayloadTooBigException("ADS", maxTheoreticalSize, bytesToBeWritten, values);
+        }
+        Data data = Data.of(bytes);
+        AmsPacket amsPacket = AdsWriteRequest.of(targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort, invokeId, indexGroup, indexOffset, data);
+        LOGGER.debug("encoded write request {}", amsPacket);
+        out.add(amsPacket);
+        requests.put(invokeId, msg);
+    }
+
+    private void encodeReadRequest(PlcRequestContainer<InternalPlcRequest, InternalPlcResponse> msg, List<Object> out) throws PlcException {
+        PlcReadRequest readRequest = (PlcReadRequest) msg.getRequest();
+
+        if (readRequest.getFields().size() != 1) {
+            throw new PlcProtocolException("Only one item supported");
+        }
+        PlcField field = readRequest.getFields().get(0);
+        if (field instanceof SymbolicAdsField) {
+            DirectAdsField mappedField = fieldMapping.get(field);
+            if (mappedField == null) {
+                throw new PlcProtocolException("No field mapping for " + field);
+            }
+            LOGGER.debug("Replacing {} with {}", field, mappedField);
+            field = mappedField;
+        }
+        if (!(field instanceof DirectAdsField)) {
+            throw new PlcProtocolException("PlcField not of type DirectAdsField: " + field.getClass());
+        }
+        DirectAdsField directAdsField = (DirectAdsField) field;
+        Long invokeId = Invoke.of(correlationBuilder.incrementAndGet());
+        IndexGroup indexGroup = IndexGroup.of(directAdsField.getIndexGroup());
+        IndexOffset indexOffset = IndexOffset.of(directAdsField.getIndexOffset());
+        AdsDataType adsDataType = directAdsField.getAdsDataType();
+        int numberOfElements = directAdsField.getNumberOfElements();
+        int readLength = adsDataType.getTargetByteSize() * numberOfElements;
+        Length length = Length.of(readLength);
+        AmsPacket amsPacket = AdsReadRequest.of(targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort, invokeId, indexGroup, indexOffset, length);
+        LOGGER.debug("encoded read request {}", amsPacket);
+        out.add(amsPacket);
+        requests.put(invokeId, msg);
+    }
+
+    private void encodeProprietaryRequest(PlcRequestContainer<InternalPlcRequest, InternalPlcResponse> msg, List<Object> out) throws PlcProtocolException {
+        PlcProprietaryRequest plcProprietaryRequest = (PlcProprietaryRequest) msg.getRequest();
+        if (!(plcProprietaryRequest.getProprietaryRequest() instanceof AmsPacket)) {
+            throw new PlcProtocolException("Unsupported proprietary type for this driver " + plcProprietaryRequest.getProprietaryRequest().getClass());
+        }
+        AmsPacket amsPacket = (AmsPacket) plcProprietaryRequest.getProprietaryRequest();
+        LOGGER.debug("encoded proprietary request {}", amsPacket);
+        out.add(amsPacket);
+        requests.put(amsPacket.getAmsHeader().getInvokeId().getAsLong(), msg);
+    }
+
+    @Override
+    protected void decode(ChannelHandlerContext channelHandlerContext, AmsPacket amsPacket, List<Object> out) throws Exception {
+        LOGGER.trace("(-->IN): {}, {}, {}", channelHandlerContext, amsPacket, out);
+        if (amsPacket instanceof AdsDeviceNotificationRequest) {
+            LOGGER.debug("Received notification {}", amsPacket);
+            handleAdsDeviceNotificationRequest((AdsDeviceNotificationRequest) amsPacket);
+            return;
+        }
+        PlcRequestContainer<InternalPlcRequest, InternalPlcResponse> plcRequestContainer = requests.remove(amsPacket.getAmsHeader().getInvokeId().getAsLong());
+        if (plcRequestContainer == null) {
+            LOGGER.info("Unmapped packet received {}", amsPacket);
+            return;
+        }
+        PlcRequest request = plcRequestContainer.getRequest();
+        final InternalPlcResponse response;
+
+        // Handle the response to a read request.
+        if (request instanceof PlcReadRequest) {
+            if (amsPacket instanceof AdsReadResponse) {
+                response = decodeReadResponse((AdsReadResponse) amsPacket, plcRequestContainer);
+            } else {
+                throw new PlcProtocolException("Wrong type correlated " + amsPacket);
+            }
+        } else if (request instanceof PlcWriteRequest) {
+            if (amsPacket instanceof AdsWriteResponse) {
+                response = decodeWriteResponse((AdsWriteResponse) amsPacket, plcRequestContainer);
+            } else {
+                throw new PlcProtocolException("Wrong type correlated " + amsPacket);
+            }
+        } else if (request instanceof PlcProprietaryRequest) {
+            response = decodeProprietaryResponse(amsPacket, plcRequestContainer);
+        } else {
+            response = null;
+        }
+        LOGGER.debug("Plc4x response {}", response);
+
+        // Confirm the response being handled.
+        if (response != null) {
+            plcRequestContainer.getResponseFuture().complete(response);
+        }
+    }
+
+    private void handleAdsDeviceNotificationRequest(AdsDeviceNotificationRequest adsDeviceNotificationRequest) {
+        for (Consumer<AdsDeviceNotificationRequest> deviceNotificationListener : deviceNotificationListeners) {
+            try {
+                deviceNotificationListener.accept(adsDeviceNotificationRequest);
+            } catch (RuntimeException e) {
+                LOGGER.error("Exception received from {} while handling {}", deviceNotificationListener, adsDeviceNotificationRequest, e);
+            }
+        }
+    }
+
+    public boolean addConsumer(Consumer<AdsDeviceNotificationRequest> adsDeviceNotificationRequestConsumer) {
+        return deviceNotificationListeners.add(adsDeviceNotificationRequestConsumer);
+    }
+
+    public boolean removeConsumer(Consumer<AdsDeviceNotificationRequest> adsDeviceNotificationRequestConsumer) {
+        return deviceNotificationListeners.remove(adsDeviceNotificationRequestConsumer);
+    }
+
+
+    @SuppressWarnings("unchecked")
+    private InternalPlcResponse decodeWriteResponse(AdsWriteResponse responseMessage, PlcRequestContainer<InternalPlcRequest, InternalPlcResponse> requestContainer) {
+        InternalPlcWriteRequest plcWriteRequest = (InternalPlcWriteRequest) requestContainer.getRequest();
+        PlcResponseCode responseCode = decodeResponseCode(responseMessage.getResult());
+
+        // TODO: does every item has the same ads response or is this whole aggregation broken?
+        Map<String, PlcResponseCode> responseItems = plcWriteRequest.getFieldNames()
+            .stream()
+            .collect(Collectors.toMap(
+                fieldName -> fieldName,
+                ignore -> responseCode
+            ));
+        return new DefaultPlcWriteResponse(plcWriteRequest, responseItems);
+    }
+
+    @SuppressWarnings("unchecked")
+    private InternalPlcResponse decodeReadResponse(AdsReadeResponse responseMessage, PlcRequestContainer<InternalPlcRequest, InternalPlcResponse> requestContainer) {
+        InternalPlcReadRequest plcReadRequest = (InternalPlcReadRequest) requestContainer.getRequest();
+
+        // TODO: only single requests supported for now
+        AdsField field = (AdsField) plcReadRequest.getFields().get(0);
+
+        PlcResponseCode responseCode = decodeResponseCode(responseMessage.getResult());
+        byte[] bytes = responseMessage.getData().getBytes();
+        BaseDefaultFieldItem<?> fieldItem = decodeData(field.getAdsDataType(), bytes);
+
+        // TODO: does every item has the same ads response or is this whole aggregation broken?
+        Map<String, Pair<PlcResponseCode, BaseDefaultFieldItem>> responseItems = plcReadRequest.getFieldNames()
+            .stream()
+            .collect(Collectors.toMap(
+                fieldName -> fieldName,
+                ignore -> Pair.of(responseCode, fieldItem)
+            ));
+
+        return new DefaultPlcReadResponse(plcReadRequest, responseItems);
+    }
+
+    @SuppressWarnings("unchecked")
+    private InternalPlcResponse decodeProprietaryResponse(AmsPacket amsPacket, PlcRequestContainer<InternalPlcRequest, InternalPlcResponse> plcRequestContainer) {
+        return new DefaultPlcProprietaryResponse<>((InternalPlcProprietaryRequest) plcRequestContainer.getRequest(), amsPacket);
+    }
+
+    private PlcResponseCode decodeResponseCode(Result result) {
+        switch (result.toAdsReturnCode()) {
+            case ADS_CODE_0:
+                return PlcResponseCode.OK;
+            case ADS_CODE_1:
+                return PlcResponseCode.INTERNAL_ERROR;
+            case ADS_CODE_2:
+            case ADS_CODE_3:
+            case ADS_CODE_4:
+            case ADS_CODE_5:
+                return PlcResponseCode.INTERNAL_ERROR;
+            case ADS_CODE_6:
+            case ADS_CODE_7:
+                return PlcResponseCode.INVALID_ADDRESS;
+            case ADS_CODE_8:
+            case ADS_CODE_9:
+            case ADS_CODE_10:
+            case ADS_CODE_11:
+            case ADS_CODE_12:
+            case ADS_CODE_13:
+            case ADS_CODE_14:
+            case ADS_CODE_15:
+            case ADS_CODE_16:
+            case ADS_CODE_17:
+            case ADS_CODE_18:
+            case ADS_CODE_19:
+            case ADS_CODE_20:
+            case ADS_CODE_21:
+            case ADS_CODE_22:
+            case ADS_CODE_23:
+            case ADS_CODE_24:
+            case ADS_CODE_25:
+            case ADS_CODE_26:
+            case ADS_CODE_27:
+            case ADS_CODE_28:
+            case ADS_CODE_1280:
+            case ADS_CODE_1281:
+            case ADS_CODE_1282:
+            case ADS_CODE_1283:
+            case ADS_CODE_1284:
+            case ADS_CODE_1285:
+            case ADS_CODE_1286:
+            case ADS_CODE_1287:
+            case ADS_CODE_1288:
+            case ADS_CODE_1289:
+            case ADS_CODE_1290:
+            case ADS_CODE_1291:
+            case ADS_CODE_1292:
+            case ADS_CODE_1293:
+            case ADS_CODE_1792:
+            case ADS_CODE_1793:
+            case ADS_CODE_1794:
+            case ADS_CODE_1795:
+            case ADS_CODE_1796:
+            case ADS_CODE_1797:
+            case ADS_CODE_1798:
+            case ADS_CODE_1799:
+            case ADS_CODE_1800:
+            case ADS_CODE_1801:
+            case ADS_CODE_1802:
+            case ADS_CODE_1803:
+            case ADS_CODE_1804:
+            case ADS_CODE_1805:
+            case ADS_CODE_1806:
+            case ADS_CODE_1807:
+            case ADS_CODE_1808:
+            case ADS_CODE_1809:
+            case ADS_CODE_1810:
+            case ADS_CODE_1811:
+            case ADS_CODE_1812:
+            case ADS_CODE_1813:
+            case ADS_CODE_1814:
+            case ADS_CODE_1815:
+            case ADS_CODE_1816:
+            case ADS_CODE_1817:
+            case ADS_CODE_1818:
+            case ADS_CODE_1819:
+            case ADS_CODE_1820:
+            case ADS_CODE_1821:
+            case ADS_CODE_1822:
+            case ADS_CODE_1823:
+            case ADS_CODE_1824:
+            case ADS_CODE_1825:
+            case ADS_CODE_1826:
+            case ADS_CODE_1827:
+            case ADS_CODE_1828:
+            case ADS_CODE_1836:
+            case ADS_CODE_1856:
+            case ADS_CODE_1857:
+            case ADS_CODE_1858:
+            case ADS_CODE_1859:
+            case ADS_CODE_1860:
+            case ADS_CODE_1861:
+            case ADS_CODE_1862:
+            case ADS_CODE_1863:
+            case ADS_CODE_1864:
+            case ADS_CODE_1872:
+            case ADS_CODE_1873:
+            case ADS_CODE_1874:
+            case ADS_CODE_1875:
+            case ADS_CODE_1876:
+            case ADS_CODE_1877:
+            case ADS_CODE_4096:
+            case ADS_CODE_4097:
+            case ADS_CODE_4098:
+            case ADS_CODE_4099:
+            case ADS_CODE_4100:
+            case ADS_CODE_4101:
+            case ADS_CODE_4102:
+            case ADS_CODE_4103:
+            case ADS_CODE_4104:
+            case ADS_CODE_4105:
+            case ADS_CODE_4106:
+            case ADS_CODE_4107:
+            case ADS_CODE_4108:
+            case ADS_CODE_4109:
+            case ADS_CODE_4110:
+            case ADS_CODE_4111:
+            case ADS_CODE_4112:
+            case ADS_CODE_4119:
+            case ADS_CODE_4120:
+            case ADS_CODE_4121:
+            case ADS_CODE_4122:
+            case ADS_CODE_10060:
+            case ADS_CODE_10061:
+            case ADS_CODE_10065:
+                return PlcResponseCode.INTERNAL_ERROR;
+            case UNKNOWN:
+                return PlcResponseCode.INTERNAL_ERROR;
+        }
+        throw new IllegalStateException(result.toAdsReturnCode() + " not mapped");
+    }
+
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/exception/AdsException.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/exception/AdsException.java
new file mode 100644
index 0000000..909ecc1
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/exception/AdsException.java
@@ -0,0 +1,56 @@
+/*
+ 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 org.apache.plc4x.java.amsads.protocol.exception;
+
+import org.apache.plc4x.java.api.exceptions.PlcProtocolException;
+
+public class AdsException extends PlcProtocolException {
+    private final Long invokeId;
+
+    public AdsException(Long invokeId, String message) {
+        super(message);
+        this.invokeId = invokeId;
+    }
+
+    public AdsException(Long invokeId, String message, Throwable cause) {
+        super(message, cause);
+        this.invokeId = invokeId;
+    }
+
+    public AdsException(Long invokeId, Throwable cause) {
+        super(cause);
+        this.invokeId = invokeId;
+    }
+
+    public AdsException(Long invokeId, String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+        this.invokeId = invokeId;
+    }
+
+    public Long getInvokeId() {
+        return invokeId;
+    }
+
+    @Override
+    public String toString() {
+        return "AdsException{" +
+            "invokeId=" + invokeId +
+            "} " + super.toString();
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/util/Df1FieldHandler.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/exception/AdsProtocolOverflowException.java
similarity index 50%
rename from sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/util/Df1FieldHandler.java
rename to sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/exception/AdsProtocolOverflowException.java
index 4b92194..023b467 100644
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/util/Df1FieldHandler.java
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/exception/AdsProtocolOverflowException.java
@@ -7,7 +7,7 @@
  "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
+   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
@@ -16,24 +16,17 @@
  specific language governing permissions and limitations
  under the License.
  */
-package org.apache.plc4x.java.df1.util;
 
-import org.apache.plc4x.java.api.exceptions.PlcInvalidFieldException;
-import org.apache.plc4x.java.api.model.PlcField;
-import org.apache.plc4x.java.base.connection.DefaultPlcFieldHandler;
-import org.apache.plc4x.java.base.messages.items.BaseDefaultFieldItem;
-import org.apache.plc4x.java.df1.Df1Field;
+package org.apache.plc4x.java.amsads.protocol.exception;
 
-public class Df1FieldHandler extends DefaultPlcFieldHandler {
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
 
-    @Override
-    public PlcField createField(String fieldQuery) throws PlcInvalidFieldException {
-        return Df1Field.of(fieldQuery);
+public class AdsProtocolOverflowException extends PlcRuntimeException {
+    public AdsProtocolOverflowException(Class<?> clazz, long length) {
+        super("Overflow in datatype " + clazz + " length: " + length);
     }
 
-    @Override
-    public BaseDefaultFieldItem encodeByte(PlcField field, Object[] values) {
-        return super.encodeByte(field, values);
+    public AdsProtocolOverflowException(String constantName, long expectedLength, long actualLength) {
+        super("Overflow of " + constantName + ": " + expectedLength + ". Actual " + actualLength + "bytes.");
     }
-
 }
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/DigestUtil.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/DigestUtil.java
new file mode 100644
index 0000000..fcd89bb
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/DigestUtil.java
@@ -0,0 +1,59 @@
+/*
+ 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 org.apache.plc4x.java.amsads.protocol.util;
+
+import com.github.snksoft.crc.CRC;
+import org.apache.plc4x.java.ads.api.util.ByteReadable;
+
+public class DigestUtil {
+
+    public static final CRC.Parameters CRC16 = CRC.Parameters.CRC16;
+
+    public static final CRC.Parameters CRC16_ADS = new CRC.Parameters(
+        CRC16.getWidth(),
+        CRC16.getPolynomial(),
+        0xFFFF,
+        CRC16.isReflectIn(),
+        CRC16.isReflectOut(),
+        CRC16.getFinalXor());
+
+    private static CRC crc = new CRC(CRC16_ADS);
+
+    private DigestUtil() {
+        // Utility class
+    }
+
+    public static int calculateCrc16(ByteReadable... byteReadables) {
+        if (byteReadables.length == 1) {
+            return calculateCrc16(byteReadables[0].getBytes());
+        }
+        long currentCrcValue = crc.init();
+        for (ByteReadable byteReadable : byteReadables) {
+            currentCrcValue = crc.update(currentCrcValue, byteReadable.getBytes());
+        }
+        short finalCrc = crc.finalCRC16(currentCrcValue);
+        return Short.toUnsignedInt(Short.reverseBytes(finalCrc));
+    }
+
+    public static int calculateCrc16(byte[] bytes) {
+        short finalCrc = (short) crc.calculateCRC(bytes);
+        return Short.toUnsignedInt(Short.reverseBytes(finalCrc));
+    }
+
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/LittleEndianDecoder.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/LittleEndianDecoder.java
new file mode 100644
index 0000000..0cf9f8a
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/LittleEndianDecoder.java
@@ -0,0 +1,357 @@
+/*
+ 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 org.apache.plc4x.java.amsads.protocol.util;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.plc4x.java.ads.model.AdsDataType;
+import org.apache.plc4x.java.api.exceptions.PlcUnsupportedDataTypeException;
+import org.apache.plc4x.java.base.messages.items.*;
+
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneOffset;
+import java.util.LinkedList;
+import java.util.concurrent.TimeUnit;
+
+// TODO: we might user ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).putInt(port).asArray() etc
+public class LittleEndianDecoder {
+
+    private LittleEndianDecoder() {
+        // Utility class
+    }
+
+    @SuppressWarnings("unchecked")
+    public static BaseDefaultFieldItem decodeData(AdsDataType adsDataType, byte[] adsData) {
+        ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(adsData);
+        switch (adsDataType) {
+            case BIT: {
+                LinkedList<Boolean> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    short aByte = wrappedBuffer.readUnsignedByte();
+                    values.offer(aByte != 0);
+                }
+                return new DefaultBooleanFieldItem(values.toArray(new Boolean[0]));
+            }
+            case BIT8: {
+                LinkedList<Boolean> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    short aByte = wrappedBuffer.readUnsignedByte();
+                    values.offer(aByte != 0);
+                }
+                return new DefaultBooleanFieldItem(values.toArray(new Boolean[0]));
+            }
+            case BITARR8: {
+                LinkedList<Short> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    short aByte = wrappedBuffer.readUnsignedByte();
+                    values.offer(aByte);
+                }
+                return new DefaultShortFieldItem(values.toArray(new Short[0]));
+            }
+            case BITARR16: {
+                LinkedList<Integer> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    int aLong = wrappedBuffer.readUnsignedShortLE();
+                    values.offer(aLong);
+                }
+                return new DefaultIntegerFieldItem(values.toArray(new Integer[0]));
+            }
+            case BITARR32: {
+                LinkedList<Long> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long aLong = wrappedBuffer.readUnsignedIntLE();
+                    values.offer(aLong);
+                }
+                return new DefaultLongFieldItem(values.toArray(new Long[0]));
+            }
+            case INT8: {
+                LinkedList<Byte> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    byte aLong = wrappedBuffer.readByte();
+                    values.offer(aLong);
+                }
+                return new DefaultByteFieldItem(values.toArray(new Byte[0]));
+            }
+            case INT16: {
+                LinkedList<Short> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    short aLong = wrappedBuffer.readShortLE();
+                    values.offer(aLong);
+                }
+                return new DefaultShortFieldItem(values.toArray(new Short[0]));
+            }
+            case INT32: {
+                LinkedList<Integer> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    int intLE = wrappedBuffer.readIntLE();
+                    values.offer(intLE);
+                }
+                return new DefaultIntegerFieldItem(values.toArray(new Integer[0]));
+            }
+            case INT64: {
+                LinkedList<Long> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long longLE = wrappedBuffer.readLongLE();
+                    values.offer(longLE);
+                }
+                return new DefaultLongFieldItem(values.toArray(new Long[0]));
+            }
+            case UINT8: {
+                LinkedList<Short> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    short aLong = wrappedBuffer.readUnsignedByte();
+                    values.offer(aLong);
+                }
+                return new DefaultShortFieldItem(values.toArray(new Short[0]));
+            }
+            case UINT16: {
+                LinkedList<Integer> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    int aLong = wrappedBuffer.readUnsignedShortLE();
+                    values.offer(aLong);
+                }
+                return new DefaultIntegerFieldItem(values.toArray(new Integer[0]));
+            }
+            case UINT32: {
+                LinkedList<Long> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long aLong = wrappedBuffer.readUnsignedIntLE();
+                    values.offer(aLong);
+                }
+                return new DefaultLongFieldItem(values.toArray(new Long[0]));
+            }
+            case ULINT:
+            case UINT64: {
+                LinkedList<BigInteger> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    byte[] bytes = new byte[8];
+                    wrappedBuffer.readBytes(bytes);
+                    ArrayUtils.reverse(bytes);
+                    BigInteger bigInteger = new BigInteger(ArrayUtils.insert(0, bytes, (byte) 0x0));
+                    values.offer(bigInteger);
+                }
+                return new DefaultBigIntegerFieldItem(values.toArray(new BigInteger[0]));
+            }
+            case FLOAT: {
+                LinkedList<Float> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    // FIXME: Commented out till we are able to update to the latest netty version.
+                    float aLong = wrappedBuffer.readFloat();
+                    //float aLong = wrappedBuffer.readFloatLE();
+                    values.offer(aLong);
+                }
+                return new DefaultFloatFieldItem(values.toArray(new Float[0]));
+            }
+            case DOUBLE: {
+                LinkedList<Double> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    // FIXME: Commented out till we are able to update to the latest netty version.
+                    double aLong = wrappedBuffer.readDouble();
+                    //double aLong = wrappedBuffer.readDoubleLE();
+                    values.offer(aLong);
+                }
+                return new DefaultDoubleFieldItem(values.toArray(new Double[0]));
+            }
+            case BOOL: {
+                LinkedList<Boolean> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    short aByte = wrappedBuffer.readUnsignedByte();
+                    values.offer(aByte != 0);
+                }
+                return new DefaultBooleanFieldItem(values.toArray(new Boolean[0]));
+            }
+            case BYTE: {
+                LinkedList<Short> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    short aByte = wrappedBuffer.readUnsignedByte();
+                    values.offer(aByte);
+                }
+                return new DefaultShortFieldItem(values.toArray(new Short[0]));
+            }
+            case WORD: {
+                LinkedList<Integer> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    int aByte = wrappedBuffer.readUnsignedShortLE();
+                    values.offer(aByte);
+                }
+                return new DefaultIntegerFieldItem(values.toArray(new Integer[0]));
+            }
+            case DWORD: {
+                LinkedList<Long> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long aByte = wrappedBuffer.readUnsignedIntLE();
+                    values.offer(aByte);
+                }
+                return new DefaultLongFieldItem(values.toArray(new Long[0]));
+            }
+            case SINT: {
+                LinkedList<Byte> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    byte aByte = wrappedBuffer.readByte();
+                    values.offer(aByte);
+                }
+                return new DefaultByteFieldItem(values.toArray(new Byte[0]));
+            }
+            case USINT: {
+                LinkedList<Short> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    short aByte = wrappedBuffer.readUnsignedByte();
+                    values.offer(aByte);
+                }
+                return new DefaultShortFieldItem(values.toArray(new Short[0]));
+            }
+            case INT: {
+                LinkedList<Short> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    short aByte = wrappedBuffer.readShortLE();
+                    values.offer(aByte);
+                }
+                return new DefaultShortFieldItem(values.toArray(new Short[0]));
+            }
+            case UINT: {
+                LinkedList<Integer> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    int aByte = wrappedBuffer.readUnsignedShortLE();
+                    values.offer(aByte);
+                }
+                return new DefaultIntegerFieldItem(values.toArray(new Integer[0]));
+            }
+            case DINT: {
+                LinkedList<Integer> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    int aByte = wrappedBuffer.readIntLE();
+                    values.offer(aByte);
+                }
+                return new DefaultIntegerFieldItem(values.toArray(new Integer[0]));
+            }
+            case UDINT: {
+                LinkedList<Long> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long aByte = wrappedBuffer.readUnsignedIntLE();
+                    values.offer(aByte);
+                }
+                return new DefaultLongFieldItem(values.toArray(new Long[0]));
+            }
+            case LINT: {
+                LinkedList<Long> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long aByte = wrappedBuffer.readLongLE();
+                    values.offer(aByte);
+                }
+                return new DefaultLongFieldItem(values.toArray(new Long[0]));
+            }
+            case REAL: {
+                LinkedList<Float> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    // FIXME: Commented out till we are able to update to the latest netty version.
+                    float aByte = wrappedBuffer.readFloat();
+                    //float aByte = wrappedBuffer.readFloatLE();
+                    values.offer(aByte);
+                }
+                return new DefaultFloatFieldItem(values.toArray(new Float[0]));
+            }
+            case LREAL: {
+                LinkedList<Double> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    // FIXME: Commented out till we are able to update to the latest netty version.
+                    double aByte = wrappedBuffer.readDouble();
+                    //double aByte = wrappedBuffer.readDoubleLE();
+                    values.offer(aByte);
+                }
+                return new DefaultDoubleFieldItem(values.toArray(new Double[0]));
+            }
+            case STRING: {
+                LinkedList<String> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    ByteArrayOutputStream os = new ByteArrayOutputStream();
+                    byte aByte;
+                    while ((aByte = wrappedBuffer.readByte()) != 0x0) {
+                        os.write(aByte);
+                    }
+                    values.offer(new String(os.toByteArray()));
+                }
+                return new DefaultStringFieldItem(values.toArray(new String[0]));
+            }
+            case TIME: {
+                LinkedList<Long> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long aByte = wrappedBuffer.readUnsignedIntLE();
+                    values.offer(aByte);
+                }
+                return new DefaultLongFieldItem(values.toArray(new Long[0]));
+            }
+            case TIME_OF_DAY: {
+                LinkedList<LocalTime> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long aByte = wrappedBuffer.readUnsignedIntLE();
+                    values.offer(LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(aByte)));
+                }
+                return new DefaultLocalTimeFieldItem(values.toArray(new LocalTime[0]));
+            }
+            case DATE: {
+                LinkedList<LocalDate> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long aByte = wrappedBuffer.readUnsignedIntLE();
+                    // TODO: where to get the zone offset from
+                    LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(aByte, 0, ZoneOffset.UTC);
+                    values.offer(localDateTime.toLocalDate());
+                }
+                return new DefaultLocalDateFieldItem(values.toArray(new LocalDate[0]));
+            }
+            case DATE_AND_TIME: {
+                LinkedList<LocalDateTime> values = new LinkedList<>();
+                while (wrappedBuffer.isReadable()) {
+                    long aByte = wrappedBuffer.readUnsignedIntLE();
+                    // TODO: where to get the zone offset from
+                    LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(aByte, 0, ZoneOffset.UTC);
+                    values.offer(localDateTime);
+                }
+                return new DefaultLocalDateTimeFieldItem(values.toArray(new LocalDateTime[0]));
+            }
+            case ARRAY: {
+                throw new NotImplementedException("not implemented yet " + adsDataType);
+            }
+            case POINTER: {
+                throw new NotImplementedException("not implemented yet " + adsDataType);
+            }
+            case ENUM: {
+                throw new NotImplementedException("not implemented yet " + adsDataType);
+            }
+            case STRUCT: {
+                throw new NotImplementedException("not implemented yet " + adsDataType);
+            }
+            case ALIAS: {
+                throw new NotImplementedException("not implemented yet " + adsDataType);
+            }
+            case SUB_RANGE_DATA_TYPE: {
+                throw new NotImplementedException("not implemented yet " + adsDataType);
+            }
+            case UNKNOWN:
+            default:
+                throw new PlcUnsupportedDataTypeException("Unsupported adsDataType " + adsDataType);
+        }
+    }
+
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/LittleEndianEncoder.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/LittleEndianEncoder.java
new file mode 100644
index 0000000..52852c5
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/LittleEndianEncoder.java
@@ -0,0 +1,285 @@
+/*
+ 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 org.apache.plc4x.java.amsads.protocol.util;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.plc4x.java.ads.model.AdsDataType;
+import org.apache.plc4x.java.api.exceptions.PlcProtocolException;
+import org.apache.plc4x.java.api.exceptions.PlcProtocolPayloadTooBigException;
+import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
+import org.apache.plc4x.java.api.exceptions.PlcUnsupportedDataTypeException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.time.*;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+// TODO: we might user ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).putInt(port).asArray() etc
+public class LittleEndianEncoder {
+
+    private LittleEndianEncoder() {
+        // Utility class
+    }
+
+    public static byte[] encodeData(AdsDataType adsDataType, Object... values) throws PlcProtocolException {
+        if (values.length == 0) {
+            return new byte[]{};
+        }
+        Class<?> valueType = values[0].getClass();
+        Stream<byte[]> result;
+        if (valueType == Boolean.class) {
+            result = encodeBoolean(adsDataType, Arrays.stream(values).map(Boolean.class::cast));
+        } else if (valueType == Byte.class) {
+            result = encodeByte(adsDataType, Arrays.stream(values).map(Byte.class::cast));
+        } else if (valueType == Short.class) {
+            result = encodeShort(adsDataType, Arrays.stream(values).map(Short.class::cast));
+        } else if (valueType == Integer.class) {
+            result = encodeInteger(adsDataType, Arrays.stream(values).map(Integer.class::cast));
+        } else if (valueType == Long.class) {
+            result = encodeLong(adsDataType, Arrays.stream(values).map(Long.class::cast));
+        } else if (valueType == BigInteger.class) {
+            result = encodeBigInteger(adsDataType, Arrays.stream(values).map(BigInteger.class::cast));
+        } else if (valueType == LocalTime.class) {
+            result = encodeLocalTime(adsDataType, Arrays.stream(values).map(LocalTime.class::cast));
+        } else if (valueType == LocalDate.class) {
+            result = encodeLocalDate(adsDataType, Arrays.stream(values).map(LocalDate.class::cast));
+        } else if (valueType == LocalDateTime.class) {
+            result = encodeLocalDateTime(adsDataType, Arrays.stream(values).map(LocalDateTime.class::cast));
+        } else if (valueType == Float.class) {
+            result = encodeFloat(adsDataType, Arrays.stream(values).map(Float.class::cast));
+        } else if (valueType == Double.class) {
+            result = encodeDouble(adsDataType, Arrays.stream(values).map(Double.class::cast));
+        } else if (valueType == String.class) {
+            result = encodeString(adsDataType, Arrays.stream(values).map(String.class::cast));
+        } else if (valueType == byte[].class) {
+            result = encodeByteArray(adsDataType, Arrays.stream(values).map(byte[].class::cast));
+        } else if (valueType == Byte[].class) {
+            result = encodeBigByteArray(adsDataType, Arrays.stream(values).map(Byte[].class::cast));
+        } else {
+            throw new PlcUnsupportedDataTypeException(valueType);
+        }
+
+        // TODO: maybe we can replace this by a smarter flatmap
+        try {
+            return result.collect(
+                ByteArrayOutputStream::new,
+                (bos, byteValue) -> {
+                    try {
+                        bos.write(byteValue);
+                    } catch (IOException e) {
+                        throw new PlcRuntimeException(e);
+                    }
+                },
+                (a, b) -> {
+                }).toByteArray();
+        } catch (PlcRuntimeException e) {
+            throw new PlcProtocolException("Error encoding data", e);
+        }
+    }
+
+    private static Stream<byte[]> encodeString(AdsDataType adsDataType, Stream<String> stringStream) {
+        // TODO: add boundchecks and add optional extension
+        // TODO: what do we do with utf-8 values with 2 bytes? what is the charset here?
+        return stringStream
+            .map(s -> s.getBytes(Charset.defaultCharset()))
+            // TODO: this 0 termination is from s7 but might be completly wrong in ads. Guess its a terminator
+            .map(bytes -> ArrayUtils.add(bytes, (byte) 0x0));
+    }
+
+    private static Stream<byte[]> encodeByteArray(AdsDataType adsDataType, Stream<byte[]> byteArrayStream) {
+        // TODO: add boundchecks and add optional extension
+        return byteArrayStream
+            .peek(bytes -> {
+                if (bytes.length > adsDataType.getTargetByteSize()) {
+                    throw new PlcRuntimeException(new PlcProtocolPayloadTooBigException("ads", adsDataType.getTargetByteSize(), bytes.length, bytes));
+                }
+            });
+    }
+
+    private static Stream<byte[]> encodeBigByteArray(AdsDataType adsDataType, Stream<Byte[]> byteArrayStream) {
+        // TODO: add boundchecks and add optional extension
+        return byteArrayStream.map(ArrayUtils::toPrimitive)
+            .peek(bytes -> {
+                if (bytes.length > adsDataType.getTargetByteSize()) {
+                    throw new PlcRuntimeException(new PlcProtocolPayloadTooBigException("ads", adsDataType.getTargetByteSize(), bytes.length, bytes));
+                }
+            });
+    }
+
+    private static Stream<byte[]> encodeFloat(AdsDataType adsDataType, Stream<Float> floatStream) {
+        return floatStream
+            .peek(value -> checkBound(adsDataType, value))
+            // TODO: check how ads expects this data
+            .map(Float::floatToIntBits)
+            .map(intValue -> new byte[]{
+                (byte) (intValue & 0x000000ff),
+                (byte) ((intValue & 0x0000ff00) >> 8),
+                (byte) ((intValue & 0x00ff0000) >> 16),
+                (byte) ((intValue & 0xff000000) >> 24),
+            })
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static Stream<byte[]> encodeDouble(AdsDataType adsDataType, Stream<Double> doubleStream) {
+        return doubleStream
+            .peek(value -> checkBound(adsDataType, value))
+            // TODO: check how ads expects this data
+            .map(Double::doubleToLongBits)
+            .map(longValue -> new byte[]{
+                (byte) (longValue & 0x00000000_000000ffL),
+                (byte) ((longValue & 0x00000000_0000ff00L) >> 8),
+                (byte) ((longValue & 0x00000000_00ff0000L) >> 16),
+                (byte) ((longValue & 0x00000000_ff000000L) >> 24),
+                (byte) ((longValue & 0x000000ff_00000000L) >> 32),
+                (byte) ((longValue & 0x0000ff00_00000000L) >> 40),
+                (byte) ((longValue & 0x00ff0000_00000000L) >> 48),
+                (byte) ((longValue & 0xff000000_00000000L) >> 56),
+            })
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static Stream<byte[]> encodeInteger(AdsDataType adsDataType, Stream<Integer> integerStream) {
+        return integerStream
+            .peek(value -> checkBound(adsDataType, value))
+            .map(intValue -> new byte[]{
+                (byte) (intValue & 0x000000ff),
+                (byte) ((intValue & 0x0000ff00) >> 8),
+                (byte) ((intValue & 0x00ff0000) >> 16),
+                (byte) ((intValue & 0xff000000) >> 24),
+            })
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static Stream<byte[]> encodeLong(AdsDataType adsDataType, Stream<Long> longStream) {
+        return longStream
+            .peek(value -> checkBound(adsDataType, value))
+            .map(longValue -> new byte[]{
+                (byte) (longValue & 0x00000000_000000ffL),
+                (byte) ((longValue & 0x00000000_0000ff00L) >> 8),
+                (byte) ((longValue & 0x00000000_00ff0000L) >> 16),
+                (byte) ((longValue & 0x00000000_ff000000L) >> 24),
+                (byte) ((longValue & 0x000000ff_00000000L) >> 32),
+                (byte) ((longValue & 0x0000ff00_00000000L) >> 40),
+                (byte) ((longValue & 0x00ff0000_00000000L) >> 48),
+                (byte) ((longValue & 0xff000000_00000000L) >> 56),
+            })
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static Stream<byte[]> encodeBigInteger(AdsDataType adsDataType, Stream<BigInteger> bigIntegerStream) {
+        // TODO: add boundchecks and add optional extension
+        return bigIntegerStream
+            .map(bigIntValue -> {
+                byte[] bytes = bigIntValue.toByteArray();
+                if (bytes.length > 1 && bytes[0] == 0x0) {
+                    byte[] subArray = Arrays.copyOf(ArrayUtils.subarray(bytes, 1, bytes.length), adsDataType.getTargetByteSize());
+                    ArrayUtils.reverse(subArray);
+                    return subArray;
+                } else {
+                    ArrayUtils.reverse(Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+                    return bytes;
+                }
+            })
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static Stream<byte[]> encodeLocalTime(AdsDataType adsDataType, Stream<LocalTime> localTimeStream) {
+        return localTimeStream
+            .map(localTime -> ChronoUnit.MILLIS.between(LocalTime.of(0, 0), localTime))
+            .peek(value -> checkBound(adsDataType, value))
+            .map(Long::intValue)
+            .map(time -> new byte[]{
+                (byte) (time & 0x00000000_000000ffL),
+                (byte) ((time & 0x00000000_0000ff00L) >> 8),
+                (byte) ((time & 0x00000000_00ff0000L) >> 16),
+                (byte) ((time & 0x00000000_ff000000L) >> 24),
+            })
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static Stream<byte[]> encodeLocalDate(AdsDataType adsDataType, Stream<LocalDate> localDateStream) {
+        return localDateStream
+            // TODO: fixme: which offset should we use?
+            .map(localDate -> localDate.atTime(0, 0).toInstant(ZoneOffset.UTC))
+            .map(Instant::getEpochSecond)
+            .peek(value -> checkBound(adsDataType, value))
+            .map(Long::intValue)
+            .map(time -> new byte[]{
+                (byte) (time & 0x00000000_000000ffL),
+                (byte) ((time & 0x00000000_0000ff00L) >> 8),
+                (byte) ((time & 0x00000000_00ff0000L) >> 16),
+                (byte) ((time & 0x00000000_ff000000L) >> 24),
+            })
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static Stream<byte[]> encodeLocalDateTime(AdsDataType adsDataType, Stream<LocalDateTime> localDateTimeStream) {
+        return localDateTimeStream
+            // TODO: fixme: which offset should we use?
+            .map(localDateTime -> localDateTime.toInstant(ZoneOffset.UTC))
+            .map(Instant::getEpochSecond)
+            .peek(value -> checkBound(adsDataType, value))
+            .map(time -> new byte[]{
+                (byte) (time & 0x00000000_000000ffL),
+                (byte) ((time & 0x00000000_0000ff00L) >> 8),
+                (byte) ((time & 0x00000000_00ff0000L) >> 16),
+                (byte) ((time & 0x00000000_ff000000L) >> 24),
+
+                (byte) ((time & 0x000000ff_00000000L) >> 32),
+                (byte) ((time & 0x0000ff00_00000000L) >> 40),
+                (byte) ((time & 0x00ff0000_00000000L) >> 48),
+                (byte) ((time & 0xff000000_00000000L) >> 56),
+            })
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+
+    private static Stream<byte[]> encodeShort(AdsDataType adsDataType, Stream<Short> shortStream) {
+        return shortStream
+            .peek(value -> checkBound(adsDataType, value))
+            .map(shortValue -> new byte[]{
+                (byte) (shortValue & 0x00ff),
+                (byte) ((shortValue & 0xff00) >> 8),
+            })
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static Stream<byte[]> encodeByte(AdsDataType adsDataType, Stream<Byte> byteStream) {
+        return byteStream
+            .peek(value -> checkBound(adsDataType, value))
+            .map(aByte -> new byte[]{aByte})
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static Stream<byte[]> encodeBoolean(AdsDataType adsDataType, Stream<Boolean> booleanStream) {
+        return booleanStream
+            .map(booleanValue -> new byte[]{booleanValue ? (byte) 0x01 : (byte) 0x00})
+            .map(bytes -> Arrays.copyOf(bytes, adsDataType.getTargetByteSize()));
+    }
+
+    private static void checkBound(AdsDataType adsDataType, double other) {
+        if (!adsDataType.withinBounds(other)) {
+            throw new PlcRuntimeException(other + " not within bounds of " + adsDataType);
+        }
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/SingleMessageRateLimiter.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/SingleMessageRateLimiter.java
new file mode 100644
index 0000000..89f4e8f
--- /dev/null
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/SingleMessageRateLimiter.java
@@ -0,0 +1,124 @@
+/*
+ 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 org.apache.plc4x.java.amsads.protocol.util;
+
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import io.netty.util.concurrent.ScheduledFuture;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.SocketAddress;
+import java.util.ArrayDeque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Inspired by {@code ChannelTrafficShapingHandler} this limiter ensures only one message is sent at a time.
+ */
+public class SingleMessageRateLimiter extends ChannelDuplexHandler {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(SingleMessageRateLimiter.class);
+
+    private final ArrayDeque<ToSend> messagesQueue = new ArrayDeque<>();
+
+    private AtomicBoolean messageOnTheWay = new AtomicBoolean(false);
+
+    private ScheduledFuture<?> sender;
+
+    @Override
+    public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
+        LOGGER.debug("bind({}, {}, {})", ctx, localAddress, promise);
+        super.bind(ctx, localAddress, promise);
+    }
+
+    @Override
+    public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+        LOGGER.debug("deregister({}, {})", ctx, promise);
+        super.deregister(ctx, promise);
+    }
+
+    @Override
+    public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception {
+        LOGGER.debug("connect({}, {}, {}, {})", ctx, remoteAddress, localAddress, promise);
+        sender = ctx.executor().scheduleAtFixedRate(() -> {
+            LOGGER.trace("Woke up and doing work messageOnTheWay:{}, messageQueue:{}", messageOnTheWay, messagesQueue);
+            if (!messagesQueue.isEmpty() && messageOnTheWay.compareAndSet(false, true)) {
+                ToSend pop = messagesQueue.pop();
+                LOGGER.debug("Sending {}", pop);
+                pop.channelHandlerContext.writeAndFlush(pop.objectToSend, pop.promise);
+                LOGGER.debug("Send {}", pop);
+            }
+        }, 100, 10, TimeUnit.MILLISECONDS);
+        super.connect(ctx, remoteAddress, localAddress, promise);
+    }
+
+    @Override
+    public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+        LOGGER.debug("disconnect({}, {}, {}, {})", ctx, promise);
+        sender.cancel(false);
+        super.disconnect(ctx, promise);
+    }
+
+    @Override
+    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
+        LOGGER.trace("(<--OUT): {}, {}, {}", ctx, msg, promise);
+        if (messageOnTheWay.compareAndSet(false, true)) {
+            ctx.write(msg, promise);
+        } else {
+            messagesQueue.add(new ToSend(ctx, msg, promise));
+        }
+    }
+
+    @Override
+    public void read(ChannelHandlerContext ctx) throws Exception {
+        LOGGER.trace("(-->In): {}", ctx);
+        messageOnTheWay.set(false);
+        super.read(ctx);
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        LOGGER.trace("(-->ERR): {}", ctx, cause);
+        messageOnTheWay.set(false);
+        super.exceptionCaught(ctx, cause);
+    }
+
+    private static final class ToSend {
+        final ChannelHandlerContext channelHandlerContext;
+        final Object objectToSend;
+        final ChannelPromise promise;
+
+        private ToSend(ChannelHandlerContext channelHandlerContext, Object objectToSend, ChannelPromise promise) {
+            this.channelHandlerContext = channelHandlerContext;
+            this.objectToSend = objectToSend;
+            this.promise = promise;
+        }
+
+        @Override
+        public String toString() {
+            return "ToSend{" +
+                "channelHandlerContext=" + channelHandlerContext +
+                ", objectToSend=" + objectToSend +
+                ", promise=" + promise +
+                '}';
+        }
+    }
+}
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/fields/DataType.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/package-info.java
similarity index 56%
rename from sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/fields/DataType.java
rename to sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/package-info.java
index 97b8b17..123c1c7 100644
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/fields/DataType.java
+++ b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/protocol/util/package-info.java
@@ -7,7 +7,7 @@
  "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
+   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
@@ -16,29 +16,7 @@
  specific language governing permissions and limitations
  under the License.
  */
-package org.apache.plc4x.java.amsads.fields;
-
-public enum DataType {
-    BIT((short)1),
-    BIT_STRING((short)-1),
-    BYTE_STRING((short)-1),
-    INTEGER((short)2),
-    TIMER((short)-1),
-    COUNTER((short)-1),
-    GENERAL_COUNT_STRUCTURE((short)-1),
-    FLOAT((short)-1),
-    ARRAY((short)-1),
-    ADDRESS((short)-1),
-    BINARY_CODED_DECIMAL((short)-1);
-
-    private final short length;
-
-    DataType(short length) {
-        this.length = length;
-    }
-
-    public short getLength() {
-        return length;
-    }
-
-}
+/**
+ * This package contains utils regarding endianess and crc digest.
+ */
+package org.apache.plc4x.java.amsads.protocol.util;
\ No newline at end of file
diff --git a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/util/DF1Utils.java b/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/util/DF1Utils.java
deleted file mode 100644
index 820517e..0000000
--- a/sandbox/test-java-amsads-driver/src/main/java/org/apache/plc4x/java/amsads/util/DF1Utils.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- 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 org.apache.plc4x.java.df1.util;
-
-import org.apache.plc4x.java.df1.readwrite.DF1Command;
-import org.apache.plc4x.java.df1.readwrite.DF1UnprotectedReadRequest;
-import org.apache.plc4x.java.df1.readwrite.DF1UnprotectedReadResponse;
-import org.apache.plc4x.java.utils.ParseException;
-import org.apache.plc4x.java.utils.ReadBuffer;
-import org.apache.plc4x.java.utils.WriteBuffer;
-
-public class DF1Utils {
-
-    public static short crcCheck(Object... args) {
-        short destinationAddress = (short) args[0];
-        short sourceAddress = (short) args[1];
-        DF1Command command = (DF1Command) args[2];
-        short commandDiscriminatorValues = (short) command.getDiscriminatorValues()[0];
-        short status = command.getStatus();
-        int transactionCounter = command.getTransactionCounter();
-        if(command instanceof DF1UnprotectedReadRequest) {
-            try {
-                DF1UnprotectedReadRequest unprotectedReadRequest = (DF1UnprotectedReadRequest) command;
-                WriteBuffer writeBuffer = new WriteBuffer(10, false);
-                writeBuffer.writeUnsignedShort(8, destinationAddress);
-                writeBuffer.writeUnsignedShort(8, sourceAddress);
-                writeBuffer.writeUnsignedShort(8, commandDiscriminatorValues);
-                writeBuffer.writeUnsignedShort(8, status);
-                writeBuffer.writeUnsignedInt(16, (short) transactionCounter);
-                writeBuffer.writeUnsignedInt(16, (short) unprotectedReadRequest.getAddress());
-                writeBuffer.writeUnsignedShort(8, (byte) unprotectedReadRequest.getSize());
-                writeBuffer.writeUnsignedShort(8, (byte) 0x03);
-
-                byte[] data = writeBuffer.getData();
-                return calculateCRC(data);
-
-            } catch (ParseException e) {
-                throw new RuntimeException("Something went wrong during the CRC check", e);
-            }
-        } else if(command instanceof DF1UnprotectedReadResponse) {
-            DF1UnprotectedReadResponse unprotectedReadResponseCommand = (DF1UnprotectedReadResponse) command;
-            try {
-                // TODO: size has to be dependent on actual size requested
-                WriteBuffer writeBuffer = new WriteBuffer(10, false);
-                writeBuffer.writeUnsignedShort(8, destinationAddress);
-                writeBuffer.writeUnsignedShort(8, sourceAddress);
-                writeBuffer.writeUnsignedShort(8, commandDiscriminatorValues);
-                writeBuffer.writeUnsignedShort(8, status);
-                writeBuffer.writeUnsignedInt(16, (short) transactionCounter);
-                boolean escape10 = false;
-                for (short data : unprotectedReadResponseCommand.getData()) {
-//                    if (escape10 == true){
-//                        if (data == 0x10) {
-//
-//                        }
-//                    } else{
-//
-//                    }
-                    writeBuffer.writeUnsignedShort(8,  data);
-                }
-                writeBuffer.writeUnsignedShort(8, (byte) 0x03);
-
-                byte[] data = writeBuffer.getData();
-                return calculateCRC(data);
-
-            } catch (ParseException e) {
-                throw new RuntimeException("Something went wrong during the CRC check", e);
-            }
-        }
-
-        return 0;
-    }
-
-    public static boolean dataTerminate(ReadBuffer io) {
-        try {
-            // The byte sequence 0x10 followed by 0x03 indicates the end of the message,
-            // so if we would read this, we abort the loop and stop reading data.
-            if ((io.peekByte(0) == (byte) 0x10) && (io.peekByte(1) == (byte) 0x03)) {
-                return true;
-            }
-        } catch (ParseException e) {
-            // Just ignore and return false.
-        }
-        return false;
-    }
-
-    public static short readData(ReadBuffer io) {
-        try {
-            // If we read a 0x10, this has to be followed by another 0x10, which is how
-            // this value is escaped in DF1, so if we encounter two 0x10, we simply ignore the first.
-            if ((io.peekByte(0) == (byte) 0x10) && (io.peekByte(1) == 0x10)) {
-                io.readByte(8);
-            }
-            return io.readUnsignedShort(8);
-        } catch (ParseException e) {
-            throw new RuntimeException("Error parsing data", e);
-        }
-    }
-
-    public static void writeData(WriteBuffer io, short data) {
-        try {
-            // If a value is 0x10, this has to be duplicated in order to be escaped.
-            if ((data == (short) 0x10)) {
-                io.writeUnsignedShort(8, (short) 0x10);
-            }
-            io.writeUnsignedShort(8, data);
-        } catch (ParseException e) {
-            throw new RuntimeException("Error parsing data", e);
-        }
-    }
-
-    public static int dataLength(short[] data) {
-        int i = 0;
-        for(short dataByte : data) {
-            // If a value is 0x10, this has to be duplicated which increases the message size by one.
-            if(dataByte == 0x10) {
-                i++;
-            }
-            i++;
-        }
-        return i;
-    }
-
-    private static short calculateCRC(byte[] crcBytes) {
-        int tmp = 0;
-        int crcL, crcR;
-
-        for (int newByte : crcBytes) {
-            crcL = tmp >> 8;
-            crcR = tmp & 0xFF;
-            tmp = (crcL << 8) + (newByte ^ crcR);
-            for (int j = 0; j < 8; j++)
-                if (tmp % 2 == 1) {     // check if LSB shifted out is 1 or 0
-                    tmp = tmp >> 1;
-                    tmp = tmp ^ 0xA001;
-                } else {
-                    tmp = tmp >> 1;
-                }
-        }
-        return (short)tmp;
-    }
-}