You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by cd...@apache.org on 2018/07/06 11:40:59 UTC

[incubator-plc4x] 03/03: PLC4X-38 - Implement the Ethernet/IP Protocol

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

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

commit 735431e26b3aa74bb4a502e75446808c58175dc3
Author: Christofer Dutz <ch...@c-ware.de>
AuthorDate: Fri Jul 6 13:40:52 2018 +0200

    PLC4X-38 - Implement the Ethernet/IP Protocol
---
 .../plc4x/java/ethernetip/EtherNetIpPlcDriver.java |  81 ++++
 .../connection/BaseEtherNetIpPlcConnection.java    |  93 +++++
 .../connection/EtherNetIpTcpPlcConnection.java     |  83 ++++
 .../java/ethernetip/model/EtherNetIpAddress.java   |  84 +++++
 .../ethernetip/netty/Plc4XEtherNetIpProtocol.java  | 419 +++++++++++++++++++++
 .../netty/events/EtherNetIpConnectedEvent.java     |  22 ++
 .../ethernetip/src/site/asciidoc/index.adoc        |  74 ++++
 .../java/ethernetip/ManualPlc4XEtherNetIpTest.java |  57 +++
 plc4j/protocols/pom.xml                            |   1 +
 9 files changed, 914 insertions(+)

diff --git a/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/EtherNetIpPlcDriver.java b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/EtherNetIpPlcDriver.java
new file mode 100644
index 0000000..69aa03a
--- /dev/null
+++ b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/EtherNetIpPlcDriver.java
@@ -0,0 +1,81 @@
+/*
+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.ethernetip;
+
+import org.apache.plc4x.java.api.PlcDriver;
+import org.apache.plc4x.java.api.authentication.PlcAuthentication;
+import org.apache.plc4x.java.api.connection.PlcConnection;
+import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
+import org.apache.plc4x.java.ethernetip.connection.EtherNetIpTcpPlcConnection;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Implementation of the Ethernet/IP protocol, based on the driver implementation available at:
+ * https://github.com/digitalpetri/ethernet-ip/
+ *
+ * Spec:
+ * http://read.pudn.com/downloads166/ebook/763212/EIP-CIP-V2-1.0.pdf
+ */
+public class EtherNetIpPlcDriver implements PlcDriver {
+
+    private static final Pattern ETHERNETIP_URI_PATTERN = Pattern.compile("^eip://(?<host>[\\w.]+)(:(?<port>\\d*))?(?<params>\\?.*)?");
+
+    @Override
+    public String getProtocolCode() {
+        return "eip";
+    }
+
+    @Override
+    public String getProtocolName() {
+        return "EtherNet/IP (TCP)";
+    }
+
+    @Override
+    public PlcConnection connect(String url) throws PlcConnectionException {
+        Matcher matcher = ETHERNETIP_URI_PATTERN.matcher(url);
+        if (!matcher.matches()) {
+            throw new PlcConnectionException(
+                "Connection url doesn't match the format 'eip//{port|host}'");
+        }
+
+        String host = matcher.group("host");
+        String port = matcher.group("port");
+        String params = matcher.group("params") != null ? matcher.group("params").substring(1) : null;
+        try {
+            InetAddress inetAddress = InetAddress.getByName(host);
+            if (port == null) {
+                return new EtherNetIpTcpPlcConnection(inetAddress, params);
+            } else {
+                return new EtherNetIpTcpPlcConnection(inetAddress, Integer.valueOf(port), params);
+            }
+        } catch (UnknownHostException e) {
+            throw new PlcConnectionException("Unknown host" + host, e);
+        }
+    }
+
+    @Override
+    public PlcConnection connect(String url, PlcAuthentication authentication) throws PlcConnectionException {
+        throw new PlcConnectionException("EtherNet/IP connections don't support authentication.");
+    }
+
+}
diff --git a/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/connection/BaseEtherNetIpPlcConnection.java b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/connection/BaseEtherNetIpPlcConnection.java
new file mode 100644
index 0000000..31a6c2d
--- /dev/null
+++ b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/connection/BaseEtherNetIpPlcConnection.java
@@ -0,0 +1,93 @@
+/*
+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.ethernetip.connection;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.plc4x.java.api.connection.PlcReader;
+import org.apache.plc4x.java.api.connection.PlcWriter;
+import org.apache.plc4x.java.api.messages.*;
+import org.apache.plc4x.java.api.model.Address;
+import org.apache.plc4x.java.base.connection.AbstractPlcConnection;
+import org.apache.plc4x.java.base.connection.ChannelFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.CompletableFuture;
+
+public abstract class BaseEtherNetIpPlcConnection extends AbstractPlcConnection implements PlcReader, PlcWriter {
+
+    private static final Logger logger = LoggerFactory.getLogger(BaseEtherNetIpPlcConnection.class);
+
+    protected BaseEtherNetIpPlcConnection(ChannelFactory channelFactory, String params) {
+        super(channelFactory, true);
+
+        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) {
+                        default:
+                            logger.debug("Unknown parameter {} with value {}", paramName, paramValue);
+                    }
+                } else {
+                    logger.debug("Unknown no-value parameter {}", paramName);
+                }
+            }
+        }
+    }
+
+    @Override
+    public Address parseAddress(String addressString) {
+        /*if (MaskWriteRegisterModbusAddress.ADDRESS_PATTERN.matcher(addressString).matches()) {
+            return MaskWriteRegisterModbusAddress.of(addressString);
+        } else if (ReadDiscreteInputsModbusAddress.ADDRESS_PATTERN.matcher(addressString).matches()) {
+            return ReadDiscreteInputsModbusAddress.of(addressString);
+        } else if (ReadHoldingRegistersModbusAddress.ADDRESS_PATTERN.matcher(addressString).matches()) {
+            return ReadHoldingRegistersModbusAddress.of(addressString);
+        } else if (ReadInputRegistersModbusAddress.ADDRESS_PATTERN.matcher(addressString).matches()) {
+            return ReadInputRegistersModbusAddress.of(addressString);
+        } else if (CoilAddress.ADDRESS_PATTERN.matcher(addressString).matches()) {
+            return CoilAddress.of(addressString);
+        } else if (RegisterAddress.ADDRESS_PATTERN.matcher(addressString).matches()) {
+            return RegisterAddress.of(addressString);
+        }*/
+        return null;
+    }
+
+    @Override
+    public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest) {
+        CompletableFuture<PlcReadResponse> readFuture = new CompletableFuture<>();
+        PlcRequestContainer<PlcReadRequest, PlcReadResponse> container =
+            new PlcRequestContainer<>(readRequest, readFuture);
+        channel.writeAndFlush(container);
+        return readFuture;
+    }
+
+    @Override
+    public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest writeRequest) {
+        CompletableFuture<PlcWriteResponse> writeFuture = new CompletableFuture<>();
+        PlcRequestContainer<PlcWriteRequest, PlcWriteResponse> container =
+            new PlcRequestContainer<>(writeRequest, writeFuture);
+        channel.writeAndFlush(container);
+        return writeFuture;
+    }
+
+}
diff --git a/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/connection/EtherNetIpTcpPlcConnection.java b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/connection/EtherNetIpTcpPlcConnection.java
new file mode 100644
index 0000000..2ad3448
--- /dev/null
+++ b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/connection/EtherNetIpTcpPlcConnection.java
@@ -0,0 +1,83 @@
+/*
+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.ethernetip.connection;
+
+import com.digitalpetri.enip.EnipCodec;
+import io.netty.channel.*;
+import org.apache.plc4x.java.base.connection.ChannelFactory;
+import org.apache.plc4x.java.base.connection.TcpSocketChannelFactory;
+import org.apache.plc4x.java.base.events.ConnectEvent;
+import org.apache.plc4x.java.base.events.ConnectedEvent;
+import org.apache.plc4x.java.ethernetip.netty.Plc4XEtherNetIpProtocol;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetAddress;
+import java.util.concurrent.CompletableFuture;
+
+public class EtherNetIpTcpPlcConnection extends BaseEtherNetIpPlcConnection {
+
+    // Port 44818
+    private static final int ETHERNET_IP_TCP_PORT = 0xAF12;
+
+    private static final Logger logger = LoggerFactory.getLogger(EtherNetIpTcpPlcConnection.class);
+
+    public EtherNetIpTcpPlcConnection(InetAddress address, String params) {
+        this(new TcpSocketChannelFactory(address, ETHERNET_IP_TCP_PORT), params);
+        logger.info("Configured EtherNetIpTcpPlcConnection with: host-name {}", address.getHostAddress());
+    }
+
+    public EtherNetIpTcpPlcConnection(InetAddress address, int port, String params) {
+        this(new TcpSocketChannelFactory(address, port), params);
+        logger.info("Configured EtherNetIpTcpPlcConnection with: host-name {}", address.getHostAddress());
+    }
+
+    public EtherNetIpTcpPlcConnection(ChannelFactory channelFactory, String params) {
+        super(channelFactory, params);
+    }
+
+    @Override
+    protected ChannelHandler getChannelHandler(CompletableFuture<Void> sessionSetupCompleteFuture) {
+        return new ChannelInitializer() {
+            @Override
+            protected void initChannel(Channel channel) {
+                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 EnipCodec());
+                pipeline.addLast(new Plc4XEtherNetIpProtocol());
+            }
+        };
+    }
+
+    @Override
+    protected void sendChannelCreatedEvent() {
+        // Send an event to the pipeline telling the Protocol filters what's going on.
+        channel.pipeline().fireUserEventTriggered(new ConnectEvent());
+    }
+
+}
diff --git a/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/model/EtherNetIpAddress.java b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/model/EtherNetIpAddress.java
new file mode 100644
index 0000000..85ec148
--- /dev/null
+++ b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/model/EtherNetIpAddress.java
@@ -0,0 +1,84 @@
+/*
+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.ethernetip.model;
+
+import org.apache.plc4x.java.api.model.Address;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+public abstract class EtherNetIpAddress implements Address {
+
+    public static final Pattern ADDRESS_PATTERN = Pattern.compile("(?<address>\\d+)");
+
+    private final int objectNumber;
+    private final int instanceNumber;
+    private final int attributeNumber;
+
+    private int connectionId;
+
+    public EtherNetIpAddress(int objectNumber, int instanceNumber, int attributeNumber) {
+        this.objectNumber = objectNumber;
+        this.instanceNumber = instanceNumber;
+        this.attributeNumber = attributeNumber;
+
+        this.connectionId = -1;
+    }
+
+    public int getObjectNumber() {
+        return objectNumber;
+    }
+
+    public int getInstanceNumber() {
+        return instanceNumber;
+    }
+
+    public int getAttributeNumber() {
+        return attributeNumber;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof EtherNetIpAddress)) {
+            return false;
+        }
+        EtherNetIpAddress that = (EtherNetIpAddress) o;
+        return objectNumber == that.objectNumber &&
+            instanceNumber == that.instanceNumber &&
+            attributeNumber == that.attributeNumber;
+    }
+
+    @Override
+    public int hashCode() {
+
+        return Objects.hash(objectNumber, instanceNumber, attributeNumber);
+    }
+
+    @Override
+    public String toString() {
+        return "EtherNetIpAddress{" +
+            "object-number=" + objectNumber +
+            "instance-number=" + instanceNumber +
+            "attribute-number=" + attributeNumber +
+            '}';
+    }
+}
diff --git a/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/netty/Plc4XEtherNetIpProtocol.java b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/netty/Plc4XEtherNetIpProtocol.java
new file mode 100644
index 0000000..3d6bf3d
--- /dev/null
+++ b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/netty/Plc4XEtherNetIpProtocol.java
@@ -0,0 +1,419 @@
+/*
+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.ethernetip.netty;
+
+import com.digitalpetri.enip.EnipPacket;
+import com.digitalpetri.enip.EnipStatus;
+import com.digitalpetri.enip.commands.*;
+import com.digitalpetri.enip.cpf.*;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToMessageCodec;
+import org.apache.plc4x.java.api.exceptions.PlcProtocolException;
+import org.apache.plc4x.java.api.messages.*;
+import org.apache.plc4x.java.api.model.Address;
+import org.apache.plc4x.java.base.events.ConnectEvent;
+import org.apache.plc4x.java.base.events.ConnectedEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.Charset;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+
+public class Plc4XEtherNetIpProtocol extends MessageToMessageCodec<EnipPacket, PlcRequestContainer<PlcRequest, PlcResponse>> {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Plc4XEtherNetIpProtocol.class);
+
+    private static final int SERVICE_COMMUNICATIONS_TYPE_CODE = 0x0100;
+
+    private long sessionHandle = 0;
+    private static final AtomicLong messageId = new AtomicLong();
+
+    // General information about the remote communication endpoint.
+    private CipIdentityItem identityItem;
+    // Flag to signal, if the remote communication endpoint supports encapsulation of CIP data.
+    private boolean supportsCipEncapsulation = false;
+    // Flag to indicate, if implicit IO (subscription) is generally supported by the remote communication endpoint.
+    // This is handled via separate UDP socket, which would have to be established in parallel.
+    private boolean supportsClass0Or1UdpConnections = false;
+    // Map of non-cip interfaces, that might be used for specialized IO in future versions.
+    private Map<String, Integer> nonCipInterfaces = null;
+    // In CIP we are doing explicit connected messaging, this requires every used address to be registered at the
+    // remote server and to use that Addresses connectionId for accessing data. We are saving the references to
+    // these here.
+    // REMARK: Eventually we should add a timeout to these so we unregister them after not being used
+    // for quire some time. Hereby freeing resources on both client and server.
+    private Map<Address, Long> addressConnectionMap = new ConcurrentHashMap<>();
+
+    private final Map<Long, PlcRequestContainer<PlcRequest, PlcResponse>> requestsMap = new ConcurrentHashMap<>();
+
+    public Plc4XEtherNetIpProtocol() {
+    }
+
+    /**
+     * If the IsoTP protocol is used on top of the ISO on TCP protocol, then as soon as the pipeline receives the
+     * request to connect, an IsoTP connection request TPDU must be sent in order to initialize the connection.
+     *
+     * @param ctx the current protocol layers context
+     * @param evt the event
+     * @throws Exception throws an exception if something goes wrong internally
+     */
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        // If the connection has just been established, start setting up the connection
+        // by sending a connection request to the plc.
+        if (evt instanceof ConnectEvent) {
+            LOGGER.debug("EtherNet/IP Protocol Sending Connection Request");
+
+            EnipPacket packet = new EnipPacket(CommandCode.RegisterSession, 0, EnipStatus.EIP_SUCCESS,
+                messageId.getAndIncrement(), new RegisterSession());
+
+            ctx.channel().writeAndFlush(packet);
+        } else {
+            super.userEventTriggered(ctx, evt);
+        }
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        LOGGER.trace("(-->ERR): {}", ctx, cause);
+        super.exceptionCaught(ctx, cause);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Encoding
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void encode(ChannelHandlerContext ctx, PlcRequestContainer<PlcRequest, PlcResponse> msg, List<Object> out) {
+        LOGGER.trace("(<--OUT): {}, {}, {}", ctx, msg, out);
+        // Reset transactionId on overflow
+        messageId.compareAndSet(Short.MAX_VALUE + 1, 0);
+        PlcRequest request = msg.getRequest();
+        if (request instanceof PlcReadRequest) {
+            encodeReadRequest(msg, out);
+        } else if (request instanceof PlcWriteRequest) {
+            encodeWriteRequest(msg, out);
+        } /*else if(request instanceof PlcSubscriptionRequest) {
+            encodeSubscriptionRequest(msg, out);
+        } else if(request instanceof PlcUnsubscriptionRequest) {
+            TODO: Implement this and refactor PlcUnsubscriptionRequest first ...
+        }*/
+    }
+
+    private void encodeWriteRequest(PlcRequestContainer<PlcRequest, PlcResponse> msg, List<Object> out) {
+        if(!supportsCipEncapsulation) {
+            LOGGER.warn("CIP Encapsulation not supported by remote, payload encapsulation must be handled by target and originator");
+        }
+
+        PlcWriteRequest request = (PlcWriteRequest) msg.getRequest();
+
+        // Create a ForwardOpen CIP request
+
+        // Create EIP UnconnectedDataItemRequest
+        /*UnconnectedDataItemRequest dataItem = new UnconnectedDataItemRequest(dataEncoder);
+        CpfPacket packet = new CpfPacket(new NullAddressItem(), dataItem);
+
+        // Send that via EIP SendRRData packet
+        CompletableFuture<T> future = new CompletableFuture<>();
+
+        sendRRData(new SendRRData(packet)).whenComplete((command, ex) -> {
+            if (command != null) {
+                CpfItem[] items = command.getPacket().getItems();
+
+                if (items.length == 2 &&
+                    items[0].getTypeId() == NullAddressItem.TYPE_ID &&
+                    items[1].getTypeId() == UnconnectedDataItemResponse.TYPE_ID) {
+
+                    ByteBuf data = ((UnconnectedDataItemResponse) items[1]).getData();
+
+                    future.complete(data);
+                } else {
+                    future.completeExceptionally(new Exception("received unexpected items"));
+                }
+            } else {
+                future.completeExceptionally(ex);
+            }
+        });
+
+        channelManager.getChannel().whenComplete((ch, ex) -> {
+            if (ch != null) writeCommand(ch, command, future);
+            else future.completeExceptionally(ex);
+        });*/
+
+    }
+
+    private void encodeReadRequest(PlcRequestContainer<PlcRequest, PlcResponse> msg, List<Object> out) {
+        if(!supportsCipEncapsulation) {
+            LOGGER.warn("CIP Encapsulation not supported by remote, payload encapsulation must be handled by target and originator");
+        }
+
+        PlcReadRequest request = (PlcReadRequest) msg.getRequest();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // Decoding
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void decode(ChannelHandlerContext ctx, EnipPacket msg, List<Object> out) {
+        LOGGER.trace("(-->IN): {}, {}, {}", ctx, msg, out);
+        LOGGER.debug("{}: session handle: {}, sender context: {}, EtherNetIPPacket:{}", msg, msg.getSessionHandle(), msg.getSenderContext(), msg);
+
+        EnipPacket packet = null;
+        switch (msg.getCommandCode()) {
+            case RegisterSession:
+                handleRegisterSession(ctx, msg);
+
+                // Now try getting some detailed information about the remote.
+                packet = new EnipPacket(CommandCode.ListIdentity, sessionHandle, EnipStatus.EIP_SUCCESS,
+                    messageId.getAndIncrement(), new ListIdentity());
+                break;
+
+            case UnRegisterSession:
+                handleUnregisterSession(ctx, msg);
+
+                // Spec: The receiver shall initiate a close of the underlying
+                // TCP/IP connection when it receives this command.
+                ctx.channel().disconnect();
+                break;
+
+            case ListIdentity:
+                handleListIdentity(ctx, msg);
+
+                // Now try listing the services the remote has to offer.
+                packet = new EnipPacket(CommandCode.ListServices, sessionHandle, EnipStatus.EIP_SUCCESS,
+                    messageId.getAndIncrement(), new ListServices());
+                break;
+
+            case ListInterfaces:
+                handleListInterfaces(ctx, msg);
+
+                // Here we're done connecting.
+                ctx.channel().pipeline().fireUserEventTriggered(new ConnectedEvent());
+                break;
+
+            case ListServices:
+                handleListServices(ctx, msg);
+
+                // Now try listing the interfaces the remote has to offer.
+                packet = new EnipPacket(CommandCode.ListInterfaces, sessionHandle, EnipStatus.EIP_SUCCESS,
+                    messageId.getAndIncrement(), new ListInterfaces());
+                break;
+
+            case Nop:
+                handleNop(ctx, msg);
+                break;
+
+            case SendRRData:
+                handleSendRRDataResponse(ctx, msg);
+                break;
+
+            case SendUnitData:
+                // This might be where the connected data is sent (eventually publish/subscribe communication)
+                break;
+        }
+
+        if(packet != null) {
+            ctx.channel().writeAndFlush(packet);
+        }
+    }
+
+    /**
+     * In order to do explicit connected messaging, the client has to register a session with the server.
+     * In case of a successful session registration the response will contain the sessionHandle, which is
+     * required to be used in all subsequent connected interactions.
+     *
+     * @param ctx the {@link ChannelHandlerContext} instance.
+     * @param msg the packet received from the server.
+     */
+    private void handleRegisterSession(ChannelHandlerContext ctx, EnipPacket msg) {
+        if(msg.getStatus() == EnipStatus.EIP_SUCCESS) {
+            sessionHandle = msg.getSessionHandle();
+
+            LOGGER.info("EtherNet/IP session registered session-handle {}", sessionHandle);
+        } else {
+            ctx.channel().pipeline().fireExceptionCaught(new PlcProtocolException("Got a non-success response."));
+        }
+    }
+
+    /**
+     * As connected operations allocate resources on the server and the client, when receiving a
+     * {@link UnRegisterSession} message (request or response) the locally allocated resources have
+     * to be released again. As the correct response to a UnRegisterSession is the closing of the
+     * connection by the receiving side, this incoming command must be a request sent from the
+     * server.
+     *
+     * @param ctx the {@link ChannelHandlerContext} instance.
+     * @param msg the packet received from the server.
+     */
+    private void handleUnregisterSession(ChannelHandlerContext ctx, EnipPacket msg) {
+        if (msg.getStatus() == EnipStatus.EIP_SUCCESS) {
+            // Reset all internal variables.
+            identityItem = null;
+            supportsCipEncapsulation = false;
+            supportsClass0Or1UdpConnections = false;
+            nonCipInterfaces = null;
+            addressConnectionMap = null;
+        } else {
+            ctx.channel().pipeline().fireExceptionCaught(new PlcProtocolException("Got a non-success response."));
+        }
+    }
+
+    /**
+     * The response to a {@link ListIdentity} command contains a lot of information about the
+     * remote counterpart. In this case we just save this information for further usage.
+     *
+     * @param ctx the {@link ChannelHandlerContext} instance.
+     * @param msg the packet received from the server.
+     */
+    private void handleListIdentity(ChannelHandlerContext ctx, EnipPacket msg) {
+        if(msg.getStatus() == EnipStatus.EIP_SUCCESS) {
+            ListIdentity listIdentityResponse = (ListIdentity) msg.getCommand();
+            if(listIdentityResponse != null) {
+                identityItem = listIdentityResponse.getIdentity().orElse(null);
+                if(identityItem != null) {
+                    LOGGER.info("Connected to: \n - product name: {} \n - serial number: {} ",
+                        identityItem.getProductName().trim(), identityItem.getSerialNumber());
+                }
+            } else {
+                identityItem = null;
+            }
+        } else {
+            ctx.channel().pipeline().fireExceptionCaught(new PlcProtocolException("Got a non-success response."));
+        }
+    }
+
+    /**
+     * Some times EtherNet/IP devices support other devices than the default one.
+     * As we are required to ev eventually reference these interfaces, build a map
+     * of all the devices the remote supports. This way we can check the validity
+     * before actually sending a request.
+     *
+     * @param ctx the {@link ChannelHandlerContext} instance.
+     * @param msg the packet received from the server.
+     */
+    private void handleListInterfaces(ChannelHandlerContext ctx, EnipPacket msg) {
+        if(msg.getStatus() == EnipStatus.EIP_SUCCESS) {
+            ListInterfaces listInterfaces = (ListInterfaces) msg.getCommand();
+            if(listInterfaces != null) {
+                // If the device supports non-CIP interfaces, this array is not empty.
+                // In this case build a map so we can access the information when sending
+                // data in RR-Requests (Request-Response).
+                if(listInterfaces.getInterfaces().length > 0) {
+                    nonCipInterfaces = new HashMap<>();
+                    for (ListInterfaces.InterfaceInformation interfaceInformation : listInterfaces.getInterfaces()) {
+                        String interfaceName = new String(
+                            interfaceInformation.getData(), Charset.forName("US-ASCII")).trim();
+                        nonCipInterfaces.put(interfaceName, interfaceInformation.hashCode());
+                    }
+                } else {
+                    nonCipInterfaces = null;
+                }
+            }
+        } else {
+            ctx.channel().pipeline().fireExceptionCaught(new PlcProtocolException("Got a non-success response."));
+        }
+    }
+
+    /**
+     * Each EtherNet/IP device can support one or more so-called `services`. At least the `Communications`
+     * service is required to be supported by every EtherNet/IP compliant device. This is used for default
+     * IO operations. Usually vendors support custom services which are adjusted to their particular needs,
+     * which might be able to provide better performance than the default. In this case we are ignoring all
+     * these as supporting these would require custom adapters on the PLC4X side. However we do inspect the
+     * capabilities of the `Communications` service to check if encapsulation of CIP data is supported and
+     * if we are able to do connected implicit communication via a parallel UDP channel.
+     *
+     * @param ctx the {@link ChannelHandlerContext} instance.
+     * @param msg the packet received from the server.
+     */
+    private void handleListServices(ChannelHandlerContext ctx, EnipPacket msg) {
+        if (msg.getStatus() == EnipStatus.EIP_SUCCESS) {
+            ListServices listServices = (ListServices) msg.getCommand();
+            if(listServices != null) {
+                for (ListServices.ServiceInformation service : listServices.getServices()) {
+                    // Check if the type code matches the communications service and if bit 5 of the
+                    // capability flags is set.
+                    if (service.getTypeCode() == SERVICE_COMMUNICATIONS_TYPE_CODE) {
+                        supportsCipEncapsulation = (service.getCapabilityFlags() & 32) != 0;
+                        supportsClass0Or1UdpConnections = (service.getCapabilityFlags() & 256) != 0;
+                    }
+                }
+            } else {
+                supportsCipEncapsulation = false;
+                supportsClass0Or1UdpConnections = false;
+            }
+        } else {
+            ctx.channel().pipeline().fireExceptionCaught(new PlcProtocolException("Got a non-success response."));
+        }
+    }
+
+    /**
+     * NOP request/responses are simple no-payload messages used to check if a connection is still
+     * available. Depending on if it's a request or reply, we simply send back a NOP Reply or not.
+     * As no reply is to be generated for an incoming NOP command, this must be a NopRequest.
+     *
+     * @param ctx the {@link ChannelHandlerContext} instance.
+     * @param msg the packet received from the server.
+     */
+    private void handleNop(ChannelHandlerContext ctx, EnipPacket msg) {
+        if(msg.getStatus() == EnipStatus.EIP_SUCCESS) {
+            Nop nop = (Nop) msg.getCommand();
+            // TODO: Reset some sort of timer ...
+        } else {
+            ctx.channel().pipeline().fireExceptionCaught(
+                new PlcProtocolException("Got a non-success flagged request."));
+        }
+    }
+
+    /**
+     * As RR Data is Request Response data and the server will not issue a request to
+     * the client, we can be pretty sure this is a response to a previously issued request.
+     * This contains the actual payload for our requests.
+     *
+     * @param ctx the {@link ChannelHandlerContext} instance.
+     * @param msg the packet received from the server.
+     */
+    private void handleSendRRDataResponse(ChannelHandlerContext ctx, EnipPacket msg) {
+        // This is where the typical request/response stuff is handled.
+        long senderContext = msg.getSenderContext();
+        PlcRequestContainer<PlcRequest, PlcResponse> plcRequestContainer = requestsMap.get(senderContext);
+        if (plcRequestContainer == null) {
+            ctx.channel().pipeline().fireExceptionCaught(
+                new PlcProtocolException("Unrelated payload received for message " + msg));
+        }
+
+        PlcRequest request = plcRequestContainer.getRequest();
+    }
+
+
+    ////////////////////////////////////////////////////////////////////////////////
+    // Encoding helpers.
+    ////////////////////////////////////////////////////////////////////////////////
+
+    ////////////////////////////////////////////////////////////////////////////////
+    // Decoding helpers.
+    ////////////////////////////////////////////////////////////////////////////////
+
+}
diff --git a/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/netty/events/EtherNetIpConnectedEvent.java b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/netty/events/EtherNetIpConnectedEvent.java
new file mode 100644
index 0000000..5142299
--- /dev/null
+++ b/plc4j/protocols/ethernetip/src/main/java/org/apache/plc4x/java/ethernetip/netty/events/EtherNetIpConnectedEvent.java
@@ -0,0 +1,22 @@
+package org.apache.plc4x.java.ethernetip.netty.events;
+/*
+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.
+*/
+
+public class EtherNetIpConnectedEvent {
+}
diff --git a/plc4j/protocols/ethernetip/src/site/asciidoc/index.adoc b/plc4j/protocols/ethernetip/src/site/asciidoc/index.adoc
new file mode 100644
index 0000000..eb169d5
--- /dev/null
+++ b/plc4j/protocols/ethernetip/src/site/asciidoc/index.adoc
@@ -0,0 +1,74 @@
+//
+//  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.
+//
+
+== EtherNet/IP
+
+`IP` in `EtherNet/IP` does not relate to the `Internet Protocol` but to `Industrial Protocol`, so EtherNet/IP does not necessarily have to go over `IP` but can be implemented on `ethernet frames`.
+This driver however will utilize `EtherNet/IP` based on `TCP` and `UDP`.
+
+`Explicit messaging`: Read explicitly addressed information (Pull) (CIP Class 3 and 2)
+`Implicit messaging`: Read information distributed by the device configuration (Push) (CIP Class 1 and 0)
+
+TCP used for Explicit Messaging
+UDP used for Implicit Messaging ("Real time" I/O)
+
+In Explicit Messaging (classic Request/Response) there are two different types of access:
+
+- Connected access: A handle 'connectionId' is assigned to an address and this is used to access the resource
+- Unconnected access: No handle is used and the address has to be sent with every request
+
+We'll concentrate on the connected access.
+
+In all cases the payload of the data items is dependent on the encapsulated protocol used.
+In our case we'll concentrate on the default CIP format.
+
+Explicitly addressed object consists of the following data:
+- Object-Number
+- Instance-Number
+- Attribute-Number
+
+Here is the location of the Spec:
+https://www.odva.org/Portals/0/Library/Publications_Numbered/PUB00123R1_Common-Industrial_Protocol_and_Family_of_CIP_Networks.pdf
+
+As EtherNet/IP is used to encapsulate CIP traffic, this spec contains a chapter on the adaption of CIP for EtherNet/IP.
+
+Another documentation I found discussing the explicit messaging thing a little better is this:
+http://www.deltamotion.com/support/webhelp/rmctools/Communications/Ethernet/Supported_Protocols/EtherNetIP/EtherNet_IP_Explicit_Messaging.htm
+https://www.odva.org/Portals/0/Library/Publications_Numbered/PUB00213R0_EtherNetIP_Developers_Guide.pdf
+
+=== Reading
+
+When reading values in a request/response type of communication (explicit), there are two options:
+
+- Unconnected access: Ideal when requesting information irregularly and not frequently. Less performant.
+- Connected access: Ideal when regularly requesting information. Higher performance, but requires multiple phases.
+
+We will concentrate on explicit connected access.
+
+In order to read/write values using connected access, first we have to get a connectionId for the resource we want to access.
+In subsequent read/write operations, we don't have to provide the address, but just provide the connection id and the server will already know what we want to access.
+
+Getting the connection id, is performed by sending an unconnected request to the server referencing the `Forward Open Service`.
+This will register the connection in the Server and this will stay alive until the session is terminated.
+So we have to make sure a session is created before using this service.
+
+Part of this request is a parameter `vendor id`.
+It turns out that this id has to be purchased from the ODVA organization.
+
+https://secure.odva.org/forms/spec-vendor-id-order-form.htm
+
+But probably we can live without a valid and registered id.
\ No newline at end of file
diff --git a/plc4j/protocols/ethernetip/src/test/java/org/apache/plc4x/java/ethernetip/ManualPlc4XEtherNetIpTest.java b/plc4j/protocols/ethernetip/src/test/java/org/apache/plc4x/java/ethernetip/ManualPlc4XEtherNetIpTest.java
new file mode 100644
index 0000000..f5b332c
--- /dev/null
+++ b/plc4j/protocols/ethernetip/src/test/java/org/apache/plc4x/java/ethernetip/ManualPlc4XEtherNetIpTest.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.ethernetip;
+
+import org.apache.plc4x.java.PlcDriverManager;
+import org.apache.plc4x.java.api.connection.PlcConnection;
+import org.apache.plc4x.java.api.connection.PlcReader;
+import org.apache.plc4x.java.api.messages.items.ReadResponseItem;
+import org.apache.plc4x.java.api.messages.specific.TypeSafePlcReadRequest;
+import org.apache.plc4x.java.api.messages.specific.TypeSafePlcReadResponse;
+import org.apache.plc4x.java.api.model.Address;
+
+import java.util.concurrent.CompletableFuture;
+
+public class ManualPlc4XEtherNetIpTest {
+
+    public static void main(String... args) {
+        String connectionUrl;
+        System.out.println("Using tcp");
+        connectionUrl = "eip://192.168.42.39:44818";
+        //connectionUrl = "eip://10.10.64.30:44818";
+        try (PlcConnection plcConnection = new PlcDriverManager().getConnection(connectionUrl)) {
+            System.out.println("PlcConnection " + plcConnection);
+
+            PlcReader reader = plcConnection.getReader().orElseThrow(() -> new RuntimeException("No Reader found"));
+
+            Address address = plcConnection.parseAddress("register:7");
+            CompletableFuture<TypeSafePlcReadResponse<Integer>> response = reader
+                .read(new TypeSafePlcReadRequest<>(Integer.class, address));
+            TypeSafePlcReadResponse<Integer> readResponse = response.get();
+            System.out.println("Response " + readResponse);
+            ReadResponseItem<Integer> responseItem = readResponse.getResponseItem().orElseThrow(() -> new RuntimeException("No Item found"));
+            System.out.println("ResponseItem " + responseItem);
+            responseItem.getValues().stream().map(integer -> "Value: " + integer).forEach(System.out::println);
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.exit(1);
+        }
+        System.exit(0);
+    }
+}
diff --git a/plc4j/protocols/pom.xml b/plc4j/protocols/pom.xml
index 292c4ac..0fca4a1 100644
--- a/plc4j/protocols/pom.xml
+++ b/plc4j/protocols/pom.xml
@@ -38,6 +38,7 @@
     <module>driver-bases</module>
 
     <module>ads</module>
+    <module>ethernetip</module>
     <module>modbus</module>
     <module>s7</module>