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) { }
}
}
}