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