/* * ModbusSim: a small Modbus TCP server (slave) for testing a CI Server Modbus station without the * real device. Java 11 single-file program: runs with the JDK that ships with CI Server, nothing * to install (a PowerShell TCP listener script is blocked by Windows antivirus/AMSI as * "malicious content", see knowledge/issues.md). * * "%TLS_ROOT_PATH%\tls\jre\bin\java.exe" ModbusSim.java [port] [registers] [folder] * defaults: 15020, 1000, the current folder. Listens on 127.0.0.1 only. * * One table of holding registers (FC03 read, FC06 / FC16 write); FC04 reads the same table as input * registers (like CI Server's own slave mirror). FC07 answers 0, FC08 echoes, others exception 01. * Register 1 = protocol address 0 (CI Server point RO:1 / RI:1 = 40001 / 30001). * * /mode.txt is read every 2 s and can be changed while it runs: * pattern raw(k) = ((k * 157) mod 30000) - 5000 (fixed, signed test values; the default) * zero all registers 0 * csv values from /values.csv: one integer per line, line k = register k * hold keep the current values (use with writes from CI Server) * stop close and exit * Messages go to the console and /sim.log. * To keep it running after an SSH session ends, start it detached, e.g. with PowerShell * Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine='...'} * Never run it on a production system or on a port a real device uses. */ import java.io.*; import java.net.*; import java.nio.file.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class ModbusSim { static int n; static int[] reg; static volatile String mode = ""; static Path folder; public static void main(String[] args) throws Exception { int port = args.length > 0 ? Integer.parseInt(args[0]) : 15020; n = args.length > 1 ? Integer.parseInt(args[1]) : 1000; folder = Paths.get(args.length > 2 ? args[2] : ".").toAbsolutePath(); reg = new int[n + 1]; Path modeFile = folder.resolve("mode.txt"); if (!Files.exists(modeFile)) Files.writeString(modeFile, "pattern"); ServerSocket server = new ServerSocket(port, 10, InetAddress.getLoopbackAddress()); log("listening on 127.0.0.1:" + port + ", " + n + " registers, pid " + ProcessHandle.current().pid()); Thread control = new Thread(() -> { while (true) { try { String m = Files.readAllLines(modeFile).stream().findFirst().orElse("").trim().toLowerCase(); if (m.equals("stop")) { log("stop requested"); System.exit(0); } if (!m.equals(mode)) { mode = m; log("mode " + m); apply(m); } Thread.sleep(2000); } catch (Exception e) { log("control: " + e); } } }); control.setDaemon(true); control.start(); while (true) { Socket c = server.accept(); new Thread(() -> serve(c)).start(); } } static synchronized void apply(String m) throws IOException { if (m.equals("pattern")) for (int k = 1; k <= n; k++) reg[k] = ((k * 157) % 30000) - 5000; else if (m.equals("zero")) java.util.Arrays.fill(reg, 0); else if (m.equals("csv")) { java.util.List lines = Files.readAllLines(folder.resolve("values.csv")); for (int k = 1; k <= n && k <= lines.size(); k++) reg[k] = Integer.parseInt(lines.get(k - 1).trim()); } } static void serve(Socket c) { String who = String.valueOf(c.getRemoteSocketAddress()); log("connect " + who); try (c; DataInputStream in = new DataInputStream(new BufferedInputStream(c.getInputStream())); OutputStream out = c.getOutputStream()) { while (true) { byte[] h = new byte[7]; in.readFully(h); int len = ((h[4] & 0xFF) << 8) | (h[5] & 0xFF); if (len < 2 || len > 256) throw new IOException("bad length " + len); byte[] pdu = new byte[len - 1]; in.readFully(pdu); byte[] r = handle(pdu); byte[] resp = new byte[7 + r.length]; System.arraycopy(h, 0, resp, 0, 4); resp[4] = (byte) ((r.length + 1) >> 8); resp[5] = (byte) (r.length + 1); resp[6] = h[6]; System.arraycopy(r, 0, resp, 7, r.length); out.write(resp); out.flush(); } } catch (EOFException e) { log("disconnect " + who); } catch (Exception e) { log("disconnect " + who + " (" + e + ")"); } } static int u16(byte[] b, int i) { return ((b[i] & 0xFF) << 8) | (b[i + 1] & 0xFF); } static synchronized byte[] handle(byte[] pdu) { int fc = pdu[0] & 0xFF; if (fc == 3 || fc == 4) { int start = u16(pdu, 1), qty = u16(pdu, 3); if (qty < 1 || qty > 125) return new byte[]{(byte) (fc | 0x80), 3}; if (start + qty > n) return new byte[]{(byte) (fc | 0x80), 2}; byte[] r = new byte[2 + 2 * qty]; r[0] = (byte) fc; r[1] = (byte) (2 * qty); for (int i = 0; i < qty; i++) { int v = reg[start + i + 1] & 0xFFFF; r[2 + 2 * i] = (byte) (v >> 8); r[3 + 2 * i] = (byte) v; } return r; } if (fc == 6) { int a = u16(pdu, 1); if (a >= n) return new byte[]{(byte) 0x86, 2}; reg[a + 1] = (short) u16(pdu, 3); return pdu.clone(); } if (fc == 16) { int start = u16(pdu, 1), qty = u16(pdu, 3); if (qty < 1 || qty > 123 || start + qty > n) return new byte[]{(byte) 0x90, 2}; for (int i = 0; i < qty; i++) reg[start + i + 1] = (short) u16(pdu, 6 + 2 * i); return new byte[]{16, pdu[1], pdu[2], pdu[3], pdu[4]}; } if (fc == 7) return new byte[]{7, 0}; if (fc == 8) return pdu.clone(); return new byte[]{(byte) (fc | 0x80), 1}; } static synchronized void log(String s) { String line = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " " + s; System.out.println(line); try { Files.writeString(folder.resolve("sim.log"), line + System.lineSeparator(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); } catch (IOException ignored) { } } }