All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.digitalpetri.enip.EtherNetIpClient Maven / Gradle / Ivy

There is a newer version: 1.5.0-RC1
Show newest version
package com.digitalpetri.enip;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import com.digitalpetri.enip.commands.Command;
import com.digitalpetri.enip.commands.CommandCode;
import com.digitalpetri.enip.commands.ListIdentity;
import com.digitalpetri.enip.commands.SendRRData;
import com.digitalpetri.enip.commands.SendUnitData;
import com.digitalpetri.enip.cpf.ConnectedDataItemResponse;
import com.digitalpetri.enip.cpf.CpfPacket;
import com.digitalpetri.enip.cpf.UnconnectedDataItemResponse;
import com.google.common.collect.Maps;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EtherNetIpClient {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final ExecutorService executor;

    private final Map> pendingRequests = Maps.newConcurrentMap();
    private final AtomicLong senderContext = new AtomicLong(0L);

    private volatile long sessionHandle;

    private final ChannelManager channelManager;

    private final EtherNetIpClientConfig config;

    public EtherNetIpClient(EtherNetIpClientConfig config) {
        this.config = config;

        executor = config.getExecutor();

        channelManager = new ChannelManager(this);
    }

    public CompletableFuture connect() {
        CompletableFuture future = new CompletableFuture<>();

        channelManager.getChannel().whenComplete((ch, ex) -> {
            if (ch != null) future.complete(EtherNetIpClient.this);
            else future.completeExceptionally(ex);
        });

        return future;
    }

    public CompletableFuture disconnect() {
        channelManager.disconnect();

        return CompletableFuture.completedFuture(this);
    }

    public String getState() {
        return channelManager.getState();
    }

    public CompletableFuture listIdentity() {
        return sendCommand(new ListIdentity());
    }

    public CompletableFuture sendRRData(SendRRData command) {
        return sendCommand(command);
    }

    public CompletableFuture sendUnitData(SendUnitData command) {
        CompletableFuture future = new CompletableFuture<>();

        channelManager.getChannel().whenComplete((ch, ex) -> {
            if (ch != null) {
                EnipPacket packet = new EnipPacket(
                        command.getCommandCode(),
                        sessionHandle,
                        EnipStatus.EIP_SUCCESS,
                        0L,
                        command);

                ch.writeAndFlush(packet).addListener(f -> {
                    if (f.isSuccess()) future.complete(null);
                    else future.completeExceptionally(f.cause());
                });
            } else {
                future.completeExceptionally(ex);
            }
        });

        return future;
    }

    public EtherNetIpClientConfig getConfig() {
        return config;
    }

    public ExecutorService getExecutor() {
        return executor;
    }

    protected  CompletableFuture sendCommand(Command command) {
        CompletableFuture future = new CompletableFuture<>();

        channelManager.getChannel().whenComplete((ch, ex) -> {
            if (ch != null) writeCommand(ch, command, future);
            else future.completeExceptionally(ex);
        });

        return future;
    }

    protected  void writeCommand(Channel channel,
                                                    Command command,
                                                    CompletableFuture future) {

        EnipPacket packet = new EnipPacket(
                command.getCommandCode(),
                sessionHandle,
                EnipStatus.EIP_SUCCESS,
                senderContext.getAndIncrement(),
                command
        );

        Timeout timeout = config.getWheelTimer().newTimeout(tt -> {
            if (tt.isCancelled()) return;
            PendingRequest p = pendingRequests.remove(packet.getSenderContext());
            if (p != null) {
                String message = String.format("senderContext=%s timed out waiting %sms for response",
                        packet.getSenderContext(), config.getTimeout().toMillis());
                p.promise.completeExceptionally(new Exception(message));
            }
        }, config.getTimeout().toMillis(), TimeUnit.MILLISECONDS);

        pendingRequests.put(packet.getSenderContext(), new PendingRequest<>(future, timeout));

        channel.writeAndFlush(packet).addListener(f -> {
            if (!f.isSuccess()) {
                PendingRequest pending = pendingRequests.remove(packet.getSenderContext());
                if (pending != null) {
                    pending.timeout.cancel();
                    pending.promise.completeExceptionally(f.cause());
                }
            }
        });
    }

    private void onChannelRead(EnipPacket packet) {
        CommandCode commandCode = packet.getCommandCode();

        if (commandCode == CommandCode.SendUnitData) {
            onUnitDataReceived((SendUnitData) packet.getCommand());
        } else {
            if (commandCode == CommandCode.RegisterSession) {
                EnipStatus status = packet.getStatus();

                if (status == EnipStatus.EIP_SUCCESS) {
                    sessionHandle = packet.getSessionHandle();
                } else {
                    sessionHandle = 0L;
                }
            }

            PendingRequest pending = pendingRequests.remove(packet.getSenderContext());

            if (pending != null) {
                pending.timeout.cancel();

                EnipStatus status = packet.getStatus();

                if (status == EnipStatus.EIP_SUCCESS) {
                    pending.promise.complete(packet.getCommand());
                } else {
                    pending.promise.completeExceptionally(new Exception("EtherNet/IP status: " + status));
                }
            } else {
                logger.debug("Received response for unknown context: {}", packet.getSenderContext());

                if (packet.getCommand() instanceof SendRRData) {
                    CpfPacket cpfPacket = ((SendRRData) packet.getCommand()).getPacket();

                    Arrays.stream(cpfPacket.getItems()).forEach(item -> {
                        if (item instanceof ConnectedDataItemResponse) {
                            ReferenceCountUtil.safeRelease(((ConnectedDataItemResponse) item).getData());
                        } else if (item instanceof UnconnectedDataItemResponse) {
                            ReferenceCountUtil.safeRelease(((UnconnectedDataItemResponse) item).getData());
                        }
                    });
                }
            }
        }
    }

    private void onChannelInactive(ChannelHandlerContext ctx) {
        logger.debug("onChannelInactive() {} <-> {}",
                ctx.channel().localAddress(), ctx.channel().remoteAddress());
    }

    private void onExceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.debug("onExceptionCaught() {} <-> {}",
                ctx.channel().localAddress(), ctx.channel().remoteAddress(), cause);

        channelManager.disconnect();
    }

    /**
     * Subclasses can override this to handle incoming
     * {@link com.digitalpetri.enip.commands.SendUnitData} commands.
     *
     * @param command the {@link com.digitalpetri.enip.commands.SendUnitData} command received.
     */
    protected void onUnitDataReceived(SendUnitData command) {
    }

    private static final class EtherNetIpClientHandler extends SimpleChannelInboundHandler {

        private final ExecutorService executor;

        private final EtherNetIpClient client;

        private EtherNetIpClientHandler(EtherNetIpClient client) {
            this.client = client;

            executor = client.getExecutor();
        }

        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, EnipPacket packet) throws Exception {
            executor.execute(() -> client.onChannelRead(packet));
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            client.onChannelInactive(ctx);

            super.channelInactive(ctx);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            client.onExceptionCaught(ctx, cause);
        }

    }

    public static CompletableFuture bootstrap(EtherNetIpClient client) {
        CompletableFuture future = new CompletableFuture<>();
        EtherNetIpClientConfig config = client.getConfig();

        Bootstrap bootstrap = new Bootstrap();

        bootstrap.group(config.getEventLoop())
                .channel(NioSocketChannel.class)
                .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) config.getTimeout().toMillis())
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new EnipCodec());
                        ch.pipeline().addLast(new EtherNetIpClientHandler(client));
                    }
                });

        config.getBootstrapConsumer().accept(bootstrap);

        bootstrap.connect(config.getHostname(), config.getPort())
                .addListener((ChannelFuture f) -> {
                    if (f.isSuccess()) {
                        future.complete(f.channel());
                    } else {
                        future.completeExceptionally(f.cause());
                    }
                });


        return future;
    }

    private static final class PendingRequest {

        private final CompletableFuture promise = new CompletableFuture<>();

        private final Timeout timeout;

        @SuppressWarnings("unchecked")
        private PendingRequest(CompletableFuture future, Timeout timeout) {
            this.timeout = timeout;

            promise.whenComplete((r, ex) -> {
                if (r != null) {
                    try {
                        future.complete((T) r);
                    } catch (ClassCastException e) {
                        future.completeExceptionally(e);
                    }
                } else {
                    future.completeExceptionally(ex);
                }
            });
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy