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 2018/02/02 09:07:16 UTC

[incubator-plc4x] branch feature/Beckhoff_ADS_protocol updated: added some basic tests and commited the missing parts

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

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


The following commit(s) were added to refs/heads/feature/Beckhoff_ADS_protocol by this push:
     new 813415f  added some basic tests and commited the missing parts
813415f is described below

commit 813415f32f73124b7b54fc2f7a87e7bf92e3acc6
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Fri Feb 2 10:07:11 2018 +0100

    added some basic tests and commited the missing parts
---
 plc4j/protocols/ads/pom.xml                        |   6 ++
 .../org/apache/plc4x/java/ads/ADSPlcDriver.java    |  15 +--
 .../plc4x/java/ads/api/generic/types/AMSNetId.java |   6 ++
 .../plc4x/java/ads/api/generic/types/AMSPort.java  |   9 +-
 .../java/ads/connection/ADSPlcConnection.java      |  26 +++++-
 .../plc4x/java/ads/netty/Plc4XADSProtocol.java     |  85 +++++++++++++++++
 .../services/org.apache.plc4x.java.api.PlcDriver   |  19 ++++
 plc4j/protocols/ads/src/site/asciidoc/index.adoc   | 101 +++++++++++++++++++++
 .../apache/plc4x/java/ads/ADSPlcDriverTest.java    |  56 ++++++++++++
 .../java/ads/connection/ADSPlcConnectionTests.java |  72 +++++++++++++++
 plc4j/protocols/ads/src/test/resources/logback.xml |  36 ++++++++
 11 files changed, 418 insertions(+), 13 deletions(-)

diff --git a/plc4j/protocols/ads/pom.xml b/plc4j/protocols/ads/pom.xml
index 273084c..2eb013e 100644
--- a/plc4j/protocols/ads/pom.xml
+++ b/plc4j/protocols/ads/pom.xml
@@ -68,6 +68,12 @@
     </dependency>
 
     <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <version>3.7</version>
+    </dependency>
+
+    <dependency>
       <groupId>commons-io</groupId>
       <artifactId>commons-io</artifactId>
       <scope>test</scope>
diff --git a/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/ADSPlcDriver.java b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/ADSPlcDriver.java
index d1bcd0d..2b9b3b6 100644
--- a/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/ADSPlcDriver.java
+++ b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/ADSPlcDriver.java
@@ -37,9 +37,10 @@ import java.util.regex.Pattern;
 public class ADSPlcDriver implements PlcDriver {
 
     private static final Pattern ADS_ADDRESS_PATTERN =
-        Pattern.compile("^(?<targetAmsNetId>" + AMSNetId.AMS_NET_ID_PATTERN + "):(?<targetAmsPort>" + AMSPort.AMS_PORT_PATTERN + ")"
-            + "/"
-            + "(?<sourceAmsNetId>" + AMSNetId.AMS_NET_ID_PATTERN + "):(?<sourceAmsPort>" + AMSPort.AMS_PORT_PATTERN + ")");
+        Pattern.compile("(?<targetAmsNetId>" + AMSNetId.AMS_NET_ID_PATTERN + "):(?<targetAmsPort>" + AMSPort.AMS_PORT_PATTERN + ")"
+            + "(/"
+            + "(?<sourceAmsNetId>" + AMSNetId.AMS_NET_ID_PATTERN + "):(?<sourceAmsPort>" + AMSPort.AMS_PORT_PATTERN + ")"
+            + ")?");
     private static final Pattern ADS_URI_PATTERN = Pattern.compile("^ads://(?<host>\\w+)/" + ADS_ADDRESS_PATTERN);
 
     @Override
@@ -57,13 +58,15 @@ public class ADSPlcDriver implements PlcDriver {
         Matcher matcher = ADS_URI_PATTERN.matcher(url);
         if (!matcher.matches()) {
             throw new PlcConnectionException(
-                "Connection url doesn't match the format 'ads://{host|ip}/{targetAmsNetId}:{targetAmsPort}/{sourceAmsNetId}:{sourceAmsPort}'");
+                "Connection url " + url + " doesn't match 'ads://{host|ip}/{targetAmsNetId}:{targetAmsPort}/{sourceAmsNetId}:{sourceAmsPort}' RAW:" + ADS_URI_PATTERN);
         }
         String host = matcher.group("host");
         AMSNetId targetAmsNetId = AMSNetId.of(matcher.group("targetAmsNetId"));
         AMSPort targetAmsPort = AMSPort.of(matcher.group("targetAmsPort"));
-        AMSNetId sourceAmsNetId = AMSNetId.of(matcher.group("sourceAmsNetId"));
-        AMSPort sourceAmsPort = AMSPort.of(matcher.group("sourceAmsPort"));
+        String sourceAmsNetIdString = matcher.group("sourceAmsNetId");
+        AMSNetId sourceAmsNetId = sourceAmsNetIdString != null ? AMSNetId.of(sourceAmsNetIdString) : null;
+        String sourceAmsPortString = matcher.group("sourceAmsPort");
+        AMSPort sourceAmsPort = sourceAmsPortString != null ? AMSPort.of(sourceAmsPortString) : null;
         return new ADSPlcConnection(host, targetAmsNetId, targetAmsPort, sourceAmsNetId, sourceAmsPort);
     }
 
diff --git a/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/api/generic/types/AMSNetId.java b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/api/generic/types/AMSNetId.java
index 73fc07d..0c5917f 100644
--- a/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/api/generic/types/AMSNetId.java
+++ b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/api/generic/types/AMSNetId.java
@@ -63,4 +63,10 @@ public class AMSNetId extends ByteValue {
         byte[] bytes = ArrayUtils.toPrimitive(Stream.of(split).map(Integer::parseInt).map(Integer::byteValue).toArray(Byte[]::new));
         return new AMSNetId(bytes);
     }
+
+    @Override
+    public String toString() {
+        byte[] bytes = getBytes();
+        return bytes[0] + "." + bytes[1] + "." + bytes[2] + "." + bytes[3] + "." + bytes[4] + "." + bytes[5];
+    }
 }
diff --git a/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/api/generic/types/AMSPort.java b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/api/generic/types/AMSPort.java
index e5e9c28..ab8a1c1 100644
--- a/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/api/generic/types/AMSPort.java
+++ b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/api/generic/types/AMSPort.java
@@ -39,13 +39,18 @@ public class AMSPort extends ByteValue {
     }
 
     public static AMSPort of(int port) {
-        return new AMSPort(ByteBuffer.allocate(NUM_BYTES).putInt(port).array());
+        return new AMSPort(ByteBuffer.allocate(NUM_BYTES).put((byte) (port & 0xff)).array());
     }
 
     public static AMSPort of(String port) {
         if (!AMS_PORT_PATTERN.matcher(port).matches()) {
             throw new IllegalArgumentException(port + " must match " + AMS_PORT_PATTERN);
         }
-        return new AMSPort(ByteBuffer.allocate(NUM_BYTES).putInt(Integer.parseInt(port)).array());
+        return of(Integer.parseInt(port));
+    }
+
+    @Override
+    public String toString() {
+        return "" + getBytes()[0];
     }
 }
diff --git a/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/connection/ADSPlcConnection.java b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/connection/ADSPlcConnection.java
index a6b7ba6..92ae31f 100644
--- a/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/connection/ADSPlcConnection.java
+++ b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/connection/ADSPlcConnection.java
@@ -47,19 +47,23 @@ public class ADSPlcConnection extends AbstractPlcConnection implements PlcReader
 
     private final String hostName;
 
-    private final int pduSize;
-
     private final AMSNetId targetAmsNetId;
+
     private final AMSPort targetAmsPort;
+
     private final AMSNetId sourceAmsNetId;
+
     private final AMSPort sourceAmsPort;
 
     private EventLoopGroup workerGroup;
     private Channel channel;
 
+    public ADSPlcConnection(String hostName, AMSNetId targetAmsNetId, AMSPort targetAmsPort) {
+        this(hostName, targetAmsNetId, targetAmsPort, null, null);
+    }
+
     public ADSPlcConnection(String hostName, AMSNetId targetAmsNetId, AMSPort targetAmsPort, AMSNetId sourceAmsNetId, AMSPort sourceAmsPort) {
         this.hostName = hostName;
-        this.pduSize = 1024;
         this.targetAmsNetId = targetAmsNetId;
         this.targetAmsPort = targetAmsPort;
         this.sourceAmsNetId = sourceAmsNetId;
@@ -70,8 +74,20 @@ public class ADSPlcConnection extends AbstractPlcConnection implements PlcReader
         return hostName;
     }
 
-    public int getPduSize() {
-        return pduSize;
+    public AMSNetId getTargetAmsNetId() {
+        return targetAmsNetId;
+    }
+
+    public AMSPort getTargetAmsPort() {
+        return targetAmsPort;
+    }
+
+    public AMSNetId getSourceAmsNetId() {
+        return sourceAmsNetId;
+    }
+
+    public AMSPort getSourceAmsPort() {
+        return sourceAmsPort;
     }
 
     @Override
diff --git a/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/netty/Plc4XADSProtocol.java b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/netty/Plc4XADSProtocol.java
new file mode 100644
index 0000000..358e33b
--- /dev/null
+++ b/plc4j/protocols/ads/src/main/java/org/apache/plc4x/java/ads/netty/Plc4XADSProtocol.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.ads.netty;
+
+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.api.exceptions.PlcException;
+import org.apache.plc4x.java.api.exceptions.PlcProtocolException;
+import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.api.messages.PlcRequest;
+import org.apache.plc4x.java.api.messages.PlcRequestContainer;
+import org.apache.plc4x.java.api.messages.PlcWriteRequest;
+import org.apache.plc4x.java.api.messages.items.ReadRequestItem;
+import org.apache.plc4x.java.api.messages.items.WriteRequestItem;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class Plc4XADSProtocol extends MessageToMessageCodec<ByteBuf, PlcRequestContainer> {
+
+    private static final AtomicInteger tpduGenerator = new AtomicInteger(1);
+
+    private Map<Short, PlcRequestContainer> requests;
+
+    public Plc4XADSProtocol() {
+        this.requests = new HashMap<>();
+    }
+
+    @Override
+    protected void encode(ChannelHandlerContext ctx, PlcRequestContainer msg, List<Object> out) throws Exception {
+        PlcRequest request = msg.getRequest();
+        if (request instanceof PlcReadRequest) {
+            encodeReadRequest(msg, out);
+        } else if (request instanceof PlcWriteRequest) {
+            encodeWriteRequest(msg, out);
+        }
+    }
+
+    private void encodeWriteRequest(PlcRequestContainer msg, List<Object> out) throws PlcException {
+        PlcWriteRequest writeRequest = (PlcWriteRequest) msg.getRequest();
+        if (writeRequest.getRequestItems().size() != 1) {
+            throw new PlcProtocolException("Only one item supported");
+        }
+        WriteRequestItem<?> writeRequestItem = writeRequest.getRequestItems().get(0);
+
+        out.add(Unpooled.buffer());
+    }
+
+    private void encodeReadRequest(PlcRequestContainer msg, List<Object> out) throws PlcException {
+        PlcReadRequest readRequest = (PlcReadRequest) msg.getRequest();
+
+        if (readRequest.getRequestItems().size() != 1) {
+            throw new PlcProtocolException("Only one item supported");
+        }
+        ReadRequestItem<?> readRequestItem = readRequest.getRequestItems().get(0);
+        out.add(Unpooled.buffer());
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
+
+    }
+
+}
diff --git a/plc4j/protocols/ads/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver b/plc4j/protocols/ads/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver
new file mode 100644
index 0000000..0cdd1b3
--- /dev/null
+++ b/plc4j/protocols/ads/src/main/resources/META-INF/services/org.apache.plc4x.java.api.PlcDriver
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+org.apache.plc4x.java.ads.ADSPlcDriver
diff --git a/plc4j/protocols/ads/src/site/asciidoc/index.adoc b/plc4j/protocols/ads/src/site/asciidoc/index.adoc
new file mode 100644
index 0000000..1f37781
--- /dev/null
+++ b/plc4j/protocols/ads/src/site/asciidoc/index.adoc
@@ -0,0 +1,101 @@
+//
+//  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.
+//
+:imagesdir: ./img/
+
+== ADS Protocol Java Implementation
+
+The current version of the ADS protocol java driver is based upon Netty 4 (http://netty.io/).
+
+As the ADS protocol running on TCP is a hierarchy of protocols, the ADS driver is implemented by providing several Protocol implementations, each encoding one layer of the protocol stack.
+
+The complete pipeline for the ADS protocol looks like this:
+
+[blockdiag,ads-netty-pipeline]
+....
+{
+    Application -> ADSPlcConnection;
+    Socket -> PLC;
+
+    group {
+        label = "PLC4X ADS Driver"
+        color = "#77FF77";
+
+        ADSPlcConnection -> Plc4XADSProtocol -> ADSProtocol -> IsoTPProtocol -> IsoOnTcpProtocol -> Socket
+
+        group {
+            Plc4XADSProtocol; ADSProtocol; IsoTPProtocol; IsoOnTcpProtocol;
+        }
+    }
+}
+....
+
+Each protocol layer implementation extends the class `MessageToMessageCodec` and is configured to consume/encode messages in the current format into messages of the next lower layer.
+
+The highest layer hereby consumes PLC4X messages, which are defined in the `plc4j-api` module, and the lowest layer produces a simple byte output.
+
+In order to implement the ADS Protocol as described in link:../../../protocols/ads/index.html[ADS Protocol] internally this is what happens inside the driver as soon as a connection is requested:
+
+[seqdiag,ads-netty-setup-communication]
+....
+{
+    Application; ADSPlcConnection; Plc4XADSProtocol; ADSProtocol; IsoTPProtocol; IsoOnTcpProtocol; Socket; PLC;
+
+    group Pipeline {
+       "Plc4XADSProtocol";
+        "ADSProtocol";
+        "IsoTPProtocol";
+        "IsoOnTcpProtocol";
+    }
+
+    Application -> ADSPlcConnection [label = "Calls 'connect'"]
+    ADSPlcConnection --> IsoTPProtocol [label = "Fires ADSConnectionEvent(INITIAL) event to the pipeline", note = "IsoTPProtocol listens for ADSConnectionEvent(INITIAL) events"]
+    IsoTPProtocol -> IsoOnTcpProtocol [label = "Creates new IsoOnTcp\nconnection request message"]
+    IsoOnTcpProtocol -> Socket [label = "Creates the byte array\ncontaining the TCP on ISO\nmessage"]
+    Socket -> PLC [label = "Sends the raw byte data to\nthe PLC"]
+
+    Socket <- PLC [label = "Sends the response to the\nSocket"]
+    IsoOnTcpProtocol <- Socket [label = "Passes the binary data to\nthe pipeline"]
+    IsoTPProtocol <- IsoOnTcpProtocol [label = "Creates new IsoOnTcp message containing binary user data"]
+
+    IsoTPProtocol --> ADSProtocol [label = "Sends ADSConnectionEvent(\nISO_TP_CONNECTION_RESPONSE_\nRECEIVED) event to the\npipeline", note = "ADSProtocol listens for ADSConnectionEvent(ISO_TP_CONNECTION_RESPONSE_RECEIVED) events"]
+
+    IsoTPProtocol <- ADSProtocol [label = "Creates new ISO TP Data\nmessage containing an\n ADS SetupCommunictaion\nmessage"]
+    IsoTPProtocol -> IsoOnTcpProtocol [label = "Creates new IsoOnTcp message"]
+    IsoOnTcpProtocol -> Socket [label = "Creates the byte array\ncontaining the TCP on ISO\nmessage"]
+    Socket -> PLC [label = "Sends the raw byte data to\nthe PLC"]
+
+    Socket <-- PLC [label = "Sends the response to the\nSocket"]
+    IsoOnTcpProtocol <- Socket [label = "Passes the binary data to\nthe pipeline"]
+    IsoTPProtocol <- IsoOnTcpProtocol [label = "Creates new IsoOnTcp message containing binary user data"]
+    IsoTPProtocol -> ADSProtocol
+    ADSPlcConnection <- ADSProtocol [label = "Sends ADSConnectionEvent(SETUP_COMPLETE) event to the\npipeline", note = "ADSPlcConnection listens for ADSConnectionEvent(SETUP_COMPLETE) events"]
+
+}
+....
+
+Above picture is a simplification. The communication of the outside with the pipeline is not implemented in a way that one component directly addresses the other.
+It is more that all communication is done with the `pipeline` and Netty then takes care of calling the right component at the right time.
+
+When writing to the pipeline from an application Netty starts at the `top` of the pipeline and asks each pipeline level if it is able and willing to handle the current message.
+If it does, the message is passed in to the `encode` method and the next component is supplied with the output of this.
+If the component can't process the message the next level is checked with the unmodified message.
+
+When reading from the Socket the Netty processes the pipeline elements the same way, but in the opposite direction and a pipeline elements `decode` method is called instead of `encode`.
+
+In addition to the normal flow of messages, a Netty pipeline also provides a messaging system. Every component can fire so-called `user events` and every component can react on them by implementing a `userEventTriggered` method.
+
+This functionality is mainly used during connection setup to synchronize the sending of messages on the different layers of the protocol stack.
\ No newline at end of file
diff --git a/plc4j/protocols/ads/src/test/java/org/apache/plc4x/java/ads/ADSPlcDriverTest.java b/plc4j/protocols/ads/src/test/java/org/apache/plc4x/java/ads/ADSPlcDriverTest.java
new file mode 100644
index 0000000..49585d4
--- /dev/null
+++ b/plc4j/protocols/ads/src/test/java/org/apache/plc4x/java/ads/ADSPlcDriverTest.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.ads;
+
+
+import org.apache.plc4x.java.PlcDriverManager;
+import org.apache.plc4x.java.ads.connection.ADSPlcConnection;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.api.exceptions.PlcException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+public class ADSPlcDriverTest {
+
+    @Disabled("We first have to find/build some tool to help test these connections.")
+    @Test
+    @Tag("fast")
+    void getConnection() throws PlcException {
+        ADSPlcConnection adsConnection = (ADSPlcConnection)
+            new PlcDriverManager().getConnection("ads://localhost/0.0.0.0.0.0:13");
+        Assertions.assertEquals(adsConnection.getHostName(), "localhost");
+        Assertions.assertEquals(adsConnection.getTargetAmsNetId().toString(), "0.0.0.0.0.0");
+        Assertions.assertEquals(adsConnection.getTargetAmsPort().toString(), "13");
+    }
+
+    /**
+     * In this test case the 'ads' driver should report an invalid url format.
+     *
+     * @throws PlcException something went wrong
+     */
+    @Test
+    @Tag("fast")
+    void getConnectionInvalidUrl() throws PlcException {
+        Assertions.assertThrows(PlcConnectionException.class,
+            () -> new PlcDriverManager().getConnection("ads://localhost/hurz/2"));
+    }
+
+}
diff --git a/plc4j/protocols/ads/src/test/java/org/apache/plc4x/java/ads/connection/ADSPlcConnectionTests.java b/plc4j/protocols/ads/src/test/java/org/apache/plc4x/java/ads/connection/ADSPlcConnectionTests.java
new file mode 100644
index 0000000..8bb0d73
--- /dev/null
+++ b/plc4j/protocols/ads/src/test/java/org/apache/plc4x/java/ads/connection/ADSPlcConnectionTests.java
@@ -0,0 +1,72 @@
+/*
+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.ads.connection;
+
+import org.apache.plc4x.java.ads.api.generic.types.AMSNetId;
+import org.apache.plc4x.java.ads.api.generic.types.AMSPort;
+import org.apache.plc4x.java.ads.model.ADSAddress;
+import org.apache.plc4x.java.api.exceptions.PlcException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ADSPlcConnectionTests {
+
+    private ADSPlcConnection adsPlcConnection;
+
+    @BeforeEach
+    void setUp() {
+        adsPlcConnection = new ADSPlcConnection("localhost", AMSNetId.of("0.0.0.0.0.0"), AMSPort.of(13));
+    }
+
+    @AfterEach
+    void tearDown() {
+        adsPlcConnection = null;
+    }
+
+    @Test
+    void initialState() {
+        assertTrue(adsPlcConnection.getHostName().equalsIgnoreCase("localhost"), "Hostname is incorrect");
+        assertEquals(adsPlcConnection.getTargetAmsNetId().toString(), "0.0.0.0.0.0");
+        assertEquals(adsPlcConnection.getTargetAmsPort().toString(), "13");
+    }
+
+    @Test
+    void emptyParseAddress() {
+        try {
+            adsPlcConnection.parseAddress("");
+        } catch (PlcException exception) {
+            assertTrue(exception.getMessage().startsWith("Address string doesn't match"), "Unexpected exception");
+        }
+    }
+
+    @Test
+    void parseAddress() {
+        try {
+            ADSAddress address = (ADSAddress) adsPlcConnection.parseAddress("0.0.0.0.0.0:13");
+            assertEquals(address.targetAmsNetId.toString(), "0.0.0.0.0.0");
+            assertEquals(address.targetAmsPort.toString(), "13");
+        } catch (PlcException exception) {
+            fail("valid data block address");
+        }
+    }
+}
\ No newline at end of file
diff --git a/plc4j/protocols/ads/src/test/resources/logback.xml b/plc4j/protocols/ads/src/test/resources/logback.xml
new file mode 100644
index 0000000..bba8e02
--- /dev/null
+++ b/plc4j/protocols/ads/src/test/resources/logback.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+  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.
+
+-->
+<configuration xmlns="http://ch.qos.logback/xml/ns/logback"
+               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+               xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd">
+
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <!-- encoders are assigned the type
+         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
+    <encoder>
+      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <root level="warn">
+    <appender-ref ref="STDOUT" />
+  </root>
+
+</configuration>
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
sruehl@apache.org.