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

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

package com.digitalpetri.modbus.client;

import com.digitalpetri.modbus.ModbusRtuFrame;
import com.digitalpetri.modbus.ModbusRtuResponseFrameParser;
import com.digitalpetri.modbus.ModbusRtuResponseFrameParser.Accumulated;
import com.digitalpetri.modbus.ModbusRtuResponseFrameParser.ParserState;
import com.digitalpetri.modbus.SerialPortTransportConfig;
import com.digitalpetri.modbus.internal.util.ExecutionQueue;
import com.fazecast.jSerialComm.SerialPort;
import com.fazecast.jSerialComm.SerialPortDataListener;
import com.fazecast.jSerialComm.SerialPortEvent;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

/**
 * Modbus RTU/Serial client transport; a {@link ModbusRtuClientTransport} that sends and receives
 * {@link ModbusRtuFrame}s over a serial port.
 */
public class SerialPortClientTransport implements ModbusRtuClientTransport {

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

  private final ExecutionQueue executionQueue;

  private final SerialPort serialPort;

  private final SerialPortTransportConfig config;

  public SerialPortClientTransport(SerialPortTransportConfig config) {
    this.config = config;

    serialPort = SerialPort.getCommPort(config.serialPort());

    serialPort.setComPortParameters(
        config.baudRate(),
        config.dataBits(),
        config.stopBits(),
        config.parity(),
        config.rs485Mode()
    );

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

  @Override
  public synchronized CompletableFuture connect() {
    if (serialPort.isOpen()) {
      return CompletableFuture.completedFuture(null);
    } else {
      if (serialPort.openPort()) {
        frameParser.reset();

        // note: no-op if already added from previous connect()
        serialPort.addDataListener(new ModbusRtuDataListener());

        return CompletableFuture.completedFuture(null);
      } else {
        return CompletableFuture.failedFuture(
            new Exception(
                "failed to open port '%s', lastErrorCode=%d"
                    .formatted(config.serialPort(), serialPort.getLastErrorCode()))
        );
      }
    }
  }

  @Override
  public synchronized CompletableFuture disconnect() {
    if (serialPort.isOpen()) {
      if (serialPort.closePort()) {
        frameParser.reset();

        return CompletableFuture.completedFuture(null);
      } else {
        return CompletableFuture.failedFuture(
            new Exception(
                "failed to close port '%s', lastErrorCode=%d"
                    .formatted(config.serialPort(), serialPort.getLastErrorCode()))
        );
      }
    } else {
      return CompletableFuture.completedFuture(null);
    }
  }

  @Override
  public boolean isConnected() {
    return serialPort.isOpen();
  }

  @Override
  public CompletionStage send(ModbusRtuFrame frame) {
    ByteBuffer buffer = ByteBuffer.allocate(256);

    try {
      buffer.put((byte) frame.unitId());
      buffer.put(frame.pdu());
      buffer.put(frame.crc());

      byte[] data = new byte[buffer.position()];
      buffer.flip();
      buffer.get(data);

      int totalWritten = 0;
      while (totalWritten < data.length) {
        int written = serialPort.writeBytes(data, data.length - totalWritten, totalWritten);
        if (written == -1) {
          int errorCode = serialPort.getLastErrorCode();
          throw new Exception(
              "failed to write to port '%s', lastErrorCode=%d"
                  .formatted(config.serialPort(), errorCode));
        }
        totalWritten += written;
      }

      return CompletableFuture.completedFuture(null);
    } catch (Exception e) {
      return CompletableFuture.failedFuture(e);
    }
  }

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

  @Override
  public void resetFrameParser() {
    frameParser.reset();
  }

  private class ModbusRtuDataListener implements SerialPortDataListener {

    /**
     * Bit mask indicating what events we're interested in.
     */
    private static final int LISTENING_EVENTS = SerialPort.LISTENING_EVENT_DATA_RECEIVED;

    @Override
    public int getListeningEvents() {
      return LISTENING_EVENTS;
    }

    @Override
    public void serialEvent(SerialPortEvent event) {
      if ((event.getEventType() & LISTENING_EVENTS) == LISTENING_EVENTS) {
        onDataReceived(event);
      }
    }

    private void onDataReceived(SerialPortEvent event) {
      byte[] receivedData = event.getReceivedData();

      ParserState state = frameParser.parse(receivedData);

      if (state instanceof Accumulated a) {
        try {
          onFrameReceived(a.frame());
        } finally {
          frameParser.reset();
        }
      }
    }

    private void onFrameReceived(ModbusRtuFrame frame) {
      Consumer frameReceiver = SerialPortClientTransport.this.frameReceiver.get();
      if (frameReceiver != null) {
        executionQueue.submit(() -> frameReceiver.accept(frame));
      }
    }

  }

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

    var builder = new SerialPortTransportConfig.Builder();
    configure.accept(builder);
    return new SerialPortClientTransport(builder.build());
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy