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>