Modbus TCP PLC Simulator

Modbus TCP PLC Simulator

---------------- ModbusTCP.java ----------------

import java.util.*;
import java.util.concurrent.*;
import java.io.*;
import java.net.*;

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.*;
import javafx.scene.layout.*;
import javafx.scene.input.*;
import javafx.scene.text.*;
import javafx.scene.paint.*;
import javafx.collections.*;
import javafx.collections.transformation.*;
import javafx.beans.property.*;
import javafx.geometry.*;

public class ModbusTCP extends Application {

  int port = 502;
  int timeout = 60_000;
  boolean debug = false;

  ExecutorService executorService;
  ServerSocket serverSocket;
  Socket socket;
  ObservableList<Item> data;
  SortedList<Item> sortedList;
  TableView<Item> tableView;
  Object sync = new Object();
  volatile boolean available = true;

  @Override
  public void start(Stage stage) {

    BorderPane layout = new BorderPane();
    Scene scene = new Scene(layout, 500, 300);
    stage.setScene(scene);
    tableView = new TableView<>();
    tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
    tableView.setPlaceholder(new Label("Waiting..."));
    TableColumn<Item, Item> column = new TableColumn<>();
    tableView.widthProperty().addListener((ov, t, t1) -> {
      Pane header = (Pane)tableView.lookup("TableHeaderRow");
      if (header != null && header.isVisible()) {
        header.setMaxHeight(0); header.setMinHeight(0); header.setPrefHeight(0);
        header.setVisible(false); header.setManaged(false);
      }
    });
    tableView.setOnMousePressed(me -> {
      if (me.isPrimaryButtonDown() && me.getClickCount() == 2) {
        int ind = tableView.getSelectionModel().getSelectedIndex();
        if (ind >= 0) editItem(stage, ind);
      }
    });
    tableView.setOnKeyPressed(ke -> {
      if (ke.getCode() == KeyCode.DELETE) {
        synchronized (sync) { data.clear(); }
      } else if (ke.getCode() == KeyCode.ENTER) {
        int ind = tableView.getSelectionModel().getSelectedIndex();
        if (ind >= 0) editItem(stage, ind);
      } else if (ke.getCode() == KeyCode.F5 && ke.isAltDown()) {
        debug = !debug;
        System.out.println("Debug: " + (debug ? "on" : "off"));
      }
    });
    column.setCellValueFactory(cellData -> {
      return new ReadOnlyObjectWrapper<Item>(cellData.getValue());
    });
    Font font = Font.font("Monospaced");
    Background bg  = new Background(new BackgroundFill(Color.rgb(255, 255, 255, 0.5), null, null));
    Background bg0 = new Background(new BackgroundFill(Color.rgb(235, 235, 235, 0.5), null, null));
    Background bg1 = new Background(new BackgroundFill(Color.rgb(210, 210, 210, 0.5), null, null));
    Background bg3 = new Background(new BackgroundFill(Color.rgb(170, 170, 170, 0.5), null, null));
    Background bg4 = new Background(new BackgroundFill(Color.rgb(130, 130, 130, 0.5), null, null));
    column.setCellFactory(col -> {
      TableCell<Item, Item> cell = new TableCell<Item, Item>() {

        @Override
        protected void updateItem(Item item, boolean empty) {
          super.updateItem(item, empty);
          if (item == null || empty ) {
            setText(null);
            setBackground(bg);
          } else {
            setFont(font);
            setText(item.toString());
            setBackground(item.entity == 0 ? bg0 : item.entity == 1 ? bg1 :
                          item.entity == 3 ? bg3 : item.entity == 4 ? bg4 : bg);
          }
        }
      };
      return cell;
    });
    tableView.getColumns().add(column);
    data = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
    sortedList = new SortedList<>(data);
    tableView.setItems(sortedList);
    sortedList.comparatorProperty().bind(new SimpleObjectProperty<Comparator<Item>>((o1, o2) -> o1.compareTo(o2)));
    tableView.sortPolicyProperty().set(t -> false);
    layout.setCenter(tableView);
    stage.show();
    startServer();
    stage.setOnCloseRequest(event -> { stopServer(); });
  }

  void startServer() {
    executorService = Executors.newCachedThreadPool();
    try {
      serverSocket = new ServerSocket(port);
      executorService.submit(() -> {
        try {
          while (!Thread.currentThread().isInterrupted()) {
            socket = serverSocket.accept();
            if (!available) {
              try { socket.close(); } catch (Exception e) { }
              continue;
            }
            available = false;
            socket.setSoTimeout(timeout);
            executorService.submit(() -> {
              InputStream inStream = null;
              OutputStream outStream = null;
              if (debug) System.out.println("Connected: "+socket.toString());
              try {
                inStream = socket.getInputStream();
                outStream = socket.getOutputStream();
                byte[] inBuffer = new byte[1024];
                while (!Thread.currentThread().isInterrupted()) {
                  int n = inStream.read(inBuffer);
                  if (n < 0 || n == inBuffer.length) break;
                  if (debug) System.out.println(" Read:" + hexStr(inBuffer, n));
                  byte outBuffer[] = handle(inBuffer, n);
                  if (debug) System.out.println("Write:" + hexStr(outBuffer, outBuffer.length));
                  outStream.write(outBuffer);
                }
              } catch (SocketException e) {
              } catch (SocketTimeoutException e) {
                if (debug) System.out.println("Connection: timeout");
              } catch (Exception e) {
                e.printStackTrace();
              } finally {
                if (inStream != null) try { inStream.close(); } catch (Exception e) { }
                if (outStream != null) try { outStream.close(); } catch (Exception e) { }
                if (socket != null && !socket.isClosed()) try { socket.close(); } catch (Exception e) { }
                if (debug) System.out.println("Connection: close");
                available = true;
              }
            });
          }
        } catch (SocketException e) {
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
          stopServer();
        }
      });
    } catch (Exception e) { e.printStackTrace(); }
  }

  public String hexStr(byte[] bytes, int len) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < len; i++) sb.append(String.format(" %02X", bytes[i]));
    sb.append(" (" + len + ")");
    return sb.toString();
  }

  public void stopServer() {
    if (serverSocket != null && !serverSocket.isClosed())
      try { serverSocket.close(); } catch (Exception e) { }
    executorService.shutdownNow();
    if (socket != null && !socket.isClosed())
      try { socket.close(); } catch (Exception e) { }
  }

  byte[] handle(byte[] buf, int n) {
    if (n < 12) return err(new byte[20], 4);
    int length = toInt(buf[4], buf[5]);
    int unit = toInt(buf[6]);
    int function = toInt(buf[7]);
    int address = toInt(buf[8], buf[9]);
    int value = toInt(buf[10], buf[11]);
    if (function >= 1 && function <= 6) {
      if (length != 6) return err(buf, 4);
      if (function == 1 || function == 2) {  //Read Coil Status (1), Read Discrete Inputs (2)
        if (value > 2000 || address + value > 0xFFFF + 1) return err(buf, 2);
        int num = (value == 0 ? 0 : (value - 1) / 8 + 1);
        byte[] ret = new byte[9 + num];
        ret[0] = buf[0];
        ret[1] = buf[1];
        ret[5] = (byte)(3 + num);
        ret[6] = (byte)unit;
        ret[7] = (byte)function;
        ret[8] = (byte)num;
        for (int i = 0; i < value; i++) {
          int ind = 9 + i / 8;
          if (i % 8 == 0) ret[ind] = 0;
          if (getValue(unit, function == 1 ? 0 : 1, address + i) > 0) {
            int mask = 1 << (i % 8);
            ret[ind] |= mask;
          }
        }
        return ret;
      } else if (function == 3 || function == 4) {  //Read Holding Registers (3), Read Input Registers (4)
        if (value > 125 || address + value > 0xFFFF + 1) return err(buf, 2);
        byte[] ret = new byte[9 + value + value];
        ret[0] = buf[0];
        ret[1] = buf[1];
        ret[5] = (byte)(3 + value + value);
        ret[6] = (byte)unit;
        ret[7] = (byte)function;
        ret[8] = (byte)(value + value);
        for (int i = 0; i < value; i++) {
          int v = getValue(unit, function == 3 ? 4 : 3, address + i);
          ret[9 + i + i] = (byte)(0xFF & (v >> 8));
          ret[10 + i + i] = (byte)(0xFF & v);
        }
        return ret;
      } else if (function == 5) {   //Write Single Coil (5)
        setValue(unit, 0, address, (value & 0xFF00) == 0xFF00 ? 1 : 0);
      } else {   /*function == 6*/  //Write Single Register (6)
        setValue(unit, 4, address, value);
      }
    } else if (function == 15 || function == 16) {
      if (n < 13) return err(buf, 3);
      int quantity = toInt(buf[12]);
      if (n < quantity + 12 || length != quantity + 7) return err(buf, 3);
      if (function == 15) {   //Write Multiple Coils (15)
        if (value > 2000 || address + value > 0xFFFF + 1) return err(buf, 2);
        if (value > 0 && quantity < value / 8 + 1) return err(buf, 3);
        for (int i = 0; i < value; i++) {
          int data = toInt(buf[13 + i / 8]);
          int mask = 1 << (i % 8);
          setValue(unit, 0, address + i, (data & mask) > 0 ? 1 : 0);
        }
      } else {  /*function == 16*/  //Write Multiple Registers (16)
        if (value > 125 || address + value > 0xFFFF + 1) return err(buf, 2);
        if (value > 0 && quantity < value + value) return err(buf, 3);
        for (int i = 0; i < value; i++) {
          setValue(unit, 4, address + i, toInt(buf[13 + i + i], buf[14 + i + i]));
        }
      }
    } else return err(buf, 1);
    /*function 5, 6, 15, 16*/
    return new byte[] { buf[0], buf[1], 0, 0, 0, 6, buf[6], buf[7], buf[8], buf[9], buf[10], buf[11] };
  }

  int toInt(byte b) { return 0xFF & (int)b; }
  int toInt(byte b1, byte b2) { return (toInt(b1) << 8) + toInt(b2); }

  byte[] err(byte[] buf, int c) {
    return new byte[] { buf[0], buf[1], 0, 0, 0, 3, buf[6], (byte)(0x80 | buf[7]), (byte)c };
  }

  void setValue(int unit, int entity, int address, int value) {
    Item item = new Item(unit, entity, address, value);
    updateItem(item, true);
  }

  int getValue(int unit, int entity, int address) {
    Item item = new Item(unit, entity, address, 0);
    return updateItem(item, false);
  }

  int updateItem(Item item, boolean set) {
    int value;
    synchronized (sync) {
      int sel = tableView.getSelectionModel().getSelectedIndex();
      value = item.value;
      int i = Collections.binarySearch(sortedList, item);
      if (i < 0) {
        data.add(item);
      } else {
        i = sortedList.getSourceIndex(i);
        item = data.get(i);
        if (set) item.value = value;
        else {
          value = item.value;
          if (item.change) {
            if (item.entity == 0 || item.entity == 1) item.value = (value < 1 ? 1 : 0);
            else item.value = (value >= 0xFFFF ? 0 : value + 1);
          }
        }
        data.set(i, item);
      }
      tableView.getSelectionModel().select(sel < 0 ? 0 : sel);
    } 
    return value;
  }

  void editItem(Window owner, int itm) {
    Item item = data.get(sortedList.getSourceIndex(itm));
    Dialog<ButtonType> dialog = new Dialog<>();
    dialog.initOwner(owner);
    dialog.initStyle(StageStyle.UTILITY);
    dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
    GridPane grid = new GridPane();
    grid.setHgap(10);
    grid.setVgap(10);
    grid.setPadding(new Insets(20, 10, 10, 10));
    TextField textField = new TextField(Integer.toString(item.value));
    textField.textProperty().addListener((observable, oldValue, newValue) -> {
      if (newValue.isEmpty()) return; 
      try {
        Integer.valueOf(newValue);
      } catch (Exception e) {
        ((StringProperty)observable).setValue(oldValue);
      }
    });
    CheckBox checkBox = new CheckBox();
    checkBox.setSelected(item.change);
    grid.add(new Label("Value:"), 0, 0);
    grid.add(textField, 1, 0);
    grid.add(new Label("Auto change:"), 0, 1);
    grid.add(checkBox, 1, 1);
    dialog.getDialogPane().setContent(grid);
    Platform.runLater(() -> textField.requestFocus());
    Optional<ButtonType> result = dialog.showAndWait();
    if (result.isPresent() && result.get() == ButtonType.OK) {
      int value = 0;
      try {
        value = Integer.valueOf(textField.getText());
      } catch (Exception e) { }
      synchronized (sync) {
        if (item.entity == 0 || item.entity == 1) item.value = (value < 1 ? 0 : 1);
        else item.value = (value > 0xFFFF ? 0xFFFF : (value < 0 ? 0 : value));
        item.change = checkBox.isSelected();
        data.set(sortedList.getSourceIndex(itm), item);
        tableView.getSelectionModel().select(itm);
      }
    }
  }

  class Item implements Comparable<Item> {
    int unit, entity, address, value, index;
    boolean change = false;

    Item(int unit, int entity, int address, int value) {
      this.unit = unit;
      this.entity = entity;
      this.address = address;
      this.value = value;
      this.index = (unit << 20) | (entity << 16) | address;
    }

    public int compareTo(Item o) {
      return (this.index < o.index) ? -1 : ((this.index == o.index) ? 0 : 1);
    }

    @Override
    public String toString() {
      String s1 = (entity == 0 || entity == 1 ? "1 bit" : "16 bit");
      int val = (entity == 0 || entity == 1 ? (value < 1 ? 0 : 1) : value);
      String s2 = (entity == 1 || entity == 3 ? "r" : "w/r");
      String s3 = (change ? "a" : "");
      int addr = entity * 100_000 + address + 1;
      return String.format("%3d  %6s  %3s  %06d  %05d  %5d  %1s", unit, s1, s2, addr, address, val, s3);
    }
  }
}

------------------ Test.java -------------------

import java.io.*;
import java.net.*;
import java.lang.Thread.*;

public class Test {

  public static void main(String[] args) {

    Socket s = null;
    OutputStream os = null;
    InputStream is = null;
    try {
      s = new Socket("127.0.0.1", 502);
      s.setSoTimeout(2_000);
      os = s.getOutputStream();
      is = s.getInputStream();
      byte[][] osbuf = new byte[][] {
        {0, 0, 0, 0, 0, 6, 0,  1, 0,  0, 0,  1},
        {0, 0, 0, 0, 0, 6, 0,  2, 0,  0, 0,  1},
        {0, 0, 0, 0, 0, 6, 0,  3, 0,  0, 0,  1},
        {0, 0, 0, 0, 0, 6, 0,  4, 0,  0, 0,  1},
        {0, 0, 0, 0, 0, 6, 0,  5, 0, 10, (byte)255, 0},
        {0, 0, 0, 0, 0, 6, 0,  6, 0, 10, 0, 10},
        {0, 0, 0, 0, 0, 8, 0, 15, 0, 20, 0,  1, 1, 1},
        {0, 0, 0, 0, 0, 9, 0, 16, 0, 20, 0,  1, 2, 0, 20}
      };
      byte[] inbuf = new byte[100];
      for (int i = 0; i < osbuf.length; i++) {
        os.write(osbuf[i]);
        if (is.read(inbuf) < 0) throw new Exception();
        Thread.sleep(200);
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (os != null) try { os.close(); } catch (Exception e) { }
      if (is != null) try { is.close(); } catch (Exception e) { }
      if (s != null && !s.isClosed()) try { s.close(); } catch (Exception e) { }
    }
  }
}

Download ZIP

Back