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

com.digitalpetri.modbus.client.NettyTcpClientTransport Maven / Gradle / Ivy

package com.digitalpetri.modbus.client;

import com.digitalpetri.fsm.FsmContext;
import com.digitalpetri.modbus.ModbusTcpCodec;
import com.digitalpetri.modbus.ModbusTcpFrame;
import com.digitalpetri.modbus.internal.util.ExecutionQueue;
import com.digitalpetri.netty.fsm.ChannelActions;
import com.digitalpetri.netty.fsm.ChannelFsm;
import com.digitalpetri.netty.fsm.ChannelFsmConfig;
import com.digitalpetri.netty.fsm.ChannelFsmFactory;
import com.digitalpetri.netty.fsm.Event;
import com.digitalpetri.netty.fsm.State;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
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 java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Modbus/TCP client transport; a {@link ModbusTcpClientTransport} that sends and receives
 * {@link ModbusTcpFrame}s over TCP.
 */
public class NettyTcpClientTransport implements ModbusTcpClientTransport {

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

  private final AtomicReference> frameReceiver = new AtomicReference<>();

  private final ChannelFsm channelFsm;
  private final ExecutionQueue executionQueue;

  private final NettyClientTransportConfig config;

  public NettyTcpClientTransport(NettyClientTransportConfig config) {
    this.config = config;

    channelFsm = ChannelFsmFactory.newChannelFsm(
        ChannelFsmConfig.newBuilder()
            .setExecutor(config.executor())
            .setLazy(config.reconnectLazy())
            .setPersistent(config.connectPersistent())
            .setChannelActions(new ModbusTcpChannelActions())
            .setLoggerName("com.digitalpetri.modbus.client.ChannelFsm")
            .build()
    );

    channelFsm.addTransitionListener(
        (from, to, via) ->
            logger.debug("onStateTransition: {} -> {} via {}", from, to, via)
    );

    executionQueue = new ExecutionQueue(config.executor());
  }

  @Override
  public CompletableFuture connect() {
    return channelFsm.connect().thenApply(c -> null);
  }

  @Override
  public CompletableFuture disconnect() {
    return channelFsm.disconnect();
  }

  @Override
  public CompletionStage send(ModbusTcpFrame frame) {
    return channelFsm.getChannel().thenCompose(channel -> {
      var future = new CompletableFuture();

      channel.writeAndFlush(frame).addListener((ChannelFutureListener) channelFuture -> {
        if (channelFuture.isSuccess()) {
          future.complete(null);
        } else {
          future.completeExceptionally(channelFuture.cause());
        }
      });

      return future;
    });
  }

  @Override
  public void receive(Consumer frameReceiver) {
    this.frameReceiver.set(frameReceiver);
  }

  @Override
  public boolean isConnected() {
    return channelFsm.getState() == State.Connected;
  }

  private class ModbusTcpFrameHandler extends SimpleChannelInboundHandler {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ModbusTcpFrame frame) {
      Consumer frameReceiver = NettyTcpClientTransport.this.frameReceiver.get();
      if (frameReceiver != null) {
        executionQueue.submit(() -> frameReceiver.accept(frame));
      }
    }
  }

  private class ModbusTcpChannelActions implements ChannelActions {

    @Override
    public CompletableFuture connect(FsmContext fsmContext) {
      var bootstrap = new Bootstrap()
          .channel(NioSocketChannel.class)
          .group(config.eventLoopGroup())
          .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) config.connectTimeout().toMillis())
          .option(ChannelOption.TCP_NODELAY, Boolean.TRUE)
          .handler(new ChannelInitializer() {
            @Override
            protected void initChannel(SocketChannel channel) {
              channel.pipeline().addLast(new ModbusTcpCodec());
              channel.pipeline().addLast(new ModbusTcpFrameHandler());

              config.pipelineCustomizer().accept(channel.pipeline());
            }
          });

      config.bootstrapCustomizer().accept(bootstrap);

      var future = new CompletableFuture();

      bootstrap.connect(config.hostname(), config.port()).addListener(
          (ChannelFutureListener) channelFuture -> {
            if (channelFuture.isSuccess()) {
              future.complete(channelFuture.channel());
            } else {
              future.completeExceptionally(channelFuture.cause());
            }
          }
      );

      return future;
    }

    @Override
    public CompletableFuture disconnect(
        FsmContext fsmContext, Channel channel) {

      var future = new CompletableFuture();

      channel.close().addListener(
          (ChannelFutureListener) channelFuture ->
              future.complete(null)
      );

      return future;
    }

  }

  /**
   * Create a new {@link NettyTcpClientTransport} with a callback that allows customizing the
   * configuration.
   *
   * @param configure a {@link Consumer} that accepts a
   *     {@link NettyClientTransportConfig.Builder} instance to configure.
   * @return a new {@link NettyTcpClientTransport}.
   */
  public static NettyTcpClientTransport create(
      Consumer configure
  ) {

    var config = NettyClientTransportConfig.create(configure);

    return new NettyTcpClientTransport(config);
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy