ModbusRtuClient.java 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package com.mes.util;
  2. import com.fazecast.jSerialComm.SerialPort;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import java.io.ByteArrayOutputStream;
  6. import java.io.IOException;
  7. /**
  8. * Modbus RTU over RS232/RS485 serial port.
  9. */
  10. public class ModbusRtuClient {
  11. public static final Logger log = LoggerFactory.getLogger(ModbusRtuClient.class);
  12. private static final int INTER_FRAME_MS = 5;
  13. private final SerialPort serialPort;
  14. private final int slaveId;
  15. private final int readTimeoutMs;
  16. public ModbusRtuClient(SerialPort serialPort, int slaveId, int readTimeoutMs) {
  17. this.serialPort = serialPort;
  18. this.slaveId = slaveId & 0xFF;
  19. this.readTimeoutMs = readTimeoutMs;
  20. }
  21. public int[] readHoldingRegisters(int startAddress, int quantity) throws IOException {
  22. if (quantity <= 0 || quantity > 125) {
  23. throw new IOException("Invalid register quantity: " + quantity);
  24. }
  25. byte[] request = new byte[8];
  26. request[0] = (byte) slaveId;
  27. request[1] = 0x03;
  28. request[2] = (byte) ((startAddress >> 8) & 0xFF);
  29. request[3] = (byte) (startAddress & 0xFF);
  30. request[4] = (byte) ((quantity >> 8) & 0xFF);
  31. request[5] = (byte) (quantity & 0xFF);
  32. appendCrc(request, 6);
  33. byte[] response = transact(request);
  34. validateResponse(response, 0x03);
  35. int byteCount = response[2] & 0xFF;
  36. if (byteCount != quantity * 2) {
  37. throw new IOException("Unexpected byte count: " + byteCount);
  38. }
  39. int[] registers = new int[quantity];
  40. for (int i = 0; i < quantity; i++) {
  41. int hi = response[3 + i * 2] & 0xFF;
  42. int lo = response[4 + i * 2] & 0xFF;
  43. registers[i] = (hi << 8) | lo;
  44. }
  45. return registers;
  46. }
  47. public void writeSingleRegister(int address, int value) throws IOException {
  48. writeMultipleRegisters(address, new int[]{value & 0xFFFF});
  49. }
  50. public void writeMultipleRegisters(int startAddress, int[] values) throws IOException {
  51. if (values == null || values.length == 0 || values.length > 123) {
  52. throw new IOException("Invalid write values");
  53. }
  54. int byteCount = values.length * 2;
  55. byte[] request = new byte[9 + byteCount];
  56. request[0] = (byte) slaveId;
  57. request[1] = 0x10;
  58. request[2] = (byte) ((startAddress >> 8) & 0xFF);
  59. request[3] = (byte) (startAddress & 0xFF);
  60. request[4] = (byte) ((values.length >> 8) & 0xFF);
  61. request[5] = (byte) (values.length & 0xFF);
  62. request[6] = (byte) byteCount;
  63. for (int i = 0; i < values.length; i++) {
  64. request[7 + i * 2] = (byte) ((values[i] >> 8) & 0xFF);
  65. request[8 + i * 2] = (byte) (values[i] & 0xFF);
  66. }
  67. appendCrc(request, 7 + byteCount);
  68. byte[] response = transact(request);
  69. validateResponse(response, 0x10);
  70. }
  71. private byte[] transact(byte[] request) throws IOException {
  72. drainInput();
  73. int written = serialPort.writeBytes(request, request.length);
  74. if (written != request.length) {
  75. throw new IOException("Modbus write incomplete");
  76. }
  77. log.info("Modbus TX: {}", toHex(request));
  78. sleepQuiet(INTER_FRAME_MS);
  79. ByteArrayOutputStream out = new ByteArrayOutputStream();
  80. byte[] buffer = new byte[256];
  81. long deadline = System.currentTimeMillis() + readTimeoutMs;
  82. long lastDataTime = 0;
  83. while (System.currentTimeMillis() < deadline) {
  84. int read = serialPort.readBytes(buffer, buffer.length);
  85. if (read > 0) {
  86. out.write(buffer, 0, read);
  87. lastDataTime = System.currentTimeMillis();
  88. byte[] data = out.toByteArray();
  89. if (isResponseComplete(data)) {
  90. log.info("Modbus RX: {}", toHex(data));
  91. return data;
  92. }
  93. } else if (out.size() > 0 && lastDataTime > 0
  94. && System.currentTimeMillis() - lastDataTime > 80) {
  95. break;
  96. } else {
  97. sleepQuiet(20);
  98. }
  99. }
  100. if (out.size() == 0) {
  101. throw new IOException("Modbus response timeout");
  102. }
  103. byte[] partial = out.toByteArray();
  104. log.info("Modbus RX(partial): {}", toHex(partial));
  105. return partial;
  106. }
  107. private static boolean isResponseComplete(byte[] data) {
  108. if (data.length < 5) {
  109. return false;
  110. }
  111. int functionCode = data[1] & 0xFF;
  112. if ((functionCode & 0x80) != 0) {
  113. return data.length >= 5;
  114. }
  115. if (functionCode == 0x03) {
  116. int byteCount = data[2] & 0xFF;
  117. return data.length >= 3 + byteCount + 2;
  118. }
  119. if (functionCode == 0x10) {
  120. return data.length >= 8;
  121. }
  122. return data.length >= 5;
  123. }
  124. private static void validateResponse(byte[] response, int functionCode) throws IOException {
  125. if (response.length < 5) {
  126. throw new IOException("Modbus response too short");
  127. }
  128. if ((response[1] & 0xFF) == (functionCode | 0x80)) {
  129. throw new IOException("Modbus exception code: " + (response[2] & 0xFF));
  130. }
  131. if ((response[1] & 0xFF) != functionCode) {
  132. throw new IOException("Unexpected function code: " + (response[1] & 0xFF));
  133. }
  134. if (!verifyCrc(response)) {
  135. throw new IOException("Modbus CRC error");
  136. }
  137. }
  138. private void drainInput() {
  139. byte[] buf = new byte[256];
  140. try {
  141. while (serialPort.bytesAvailable() > 0) {
  142. serialPort.readBytes(buf, Math.min(buf.length, serialPort.bytesAvailable()));
  143. }
  144. } catch (Exception ignored) {
  145. }
  146. }
  147. static int crc16(byte[] data, int length) {
  148. int crc = 0xFFFF;
  149. for (int i = 0; i < length; i++) {
  150. crc ^= (data[i] & 0xFF);
  151. for (int j = 0; j < 8; j++) {
  152. if ((crc & 0x0001) != 0) {
  153. crc = (crc >> 1) ^ 0xA001;
  154. } else {
  155. crc >>= 1;
  156. }
  157. }
  158. }
  159. return crc & 0xFFFF;
  160. }
  161. private static void appendCrc(byte[] frame, int length) {
  162. int crc = crc16(frame, length);
  163. frame[length] = (byte) (crc & 0xFF);
  164. frame[length + 1] = (byte) ((crc >> 8) & 0xFF);
  165. }
  166. private static boolean verifyCrc(byte[] frame) {
  167. if (frame.length < 3) {
  168. return false;
  169. }
  170. int crc = crc16(frame, frame.length - 2);
  171. int lo = frame[frame.length - 2] & 0xFF;
  172. int hi = frame[frame.length - 1] & 0xFF;
  173. return crc == ((hi << 8) | lo);
  174. }
  175. static float registersToFloat(int regHi, int regLo) {
  176. int bits = ((regHi & 0xFFFF) << 16) | (regLo & 0xFFFF);
  177. return Float.intBitsToFloat(bits);
  178. }
  179. static String toHex(byte[] data) {
  180. StringBuilder sb = new StringBuilder();
  181. for (byte b : data) {
  182. sb.append(String.format("%02X ", b & 0xFF));
  183. }
  184. return sb.toString().trim();
  185. }
  186. private static void sleepQuiet(int ms) throws IOException {
  187. try {
  188. Thread.sleep(ms);
  189. } catch (InterruptedException e) {
  190. Thread.currentThread().interrupt();
  191. throw new IOException("Modbus interrupted", e);
  192. }
  193. }
  194. }