diff --git a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusRTUTransport.java b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusRTUTransport.java index a36a3d2..59c4b4a 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusRTUTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusRTUTransport.java @@ -53,20 +53,44 @@ public class ModbusRTUTransport extends ModbusSerialTransport { * @param out Output buffer to populate * @throws IOException If data cannot be read from the port */ - private void readRequestData(int byteCount, BytesOutputStream out) throws IOException { + protected void readRequestData(int byteCount, BytesOutputStream out) throws IOException { byteCount += 2; byte[] inpBuf = new byte[byteCount]; readBytes(inpBuf, byteCount); out.write(inpBuf, 0, byteCount); } + /** + * Factory method creating the required specialized ModbusRequest + * instance. + * + * @param functionCode the function code of the request as int. + * + * @return a ModbusRequest instance specific for the given function type. + */ + protected ModbusRequest createModbusRequest(int functionCode) { + return ModbusRequest.createModbusRequest(functionCode); + } + + /** + * Factory method creating the required specialized ModbusResponse + * instance. + * + * @param functionCode the function code of the response as int. + * + * @return a ModbusResponse instance specific for the given function code. + */ + protected ModbusResponse createModbusResponse(int functionCode) { + return ModbusResponse.createModbusResponse(functionCode); + } + /** * getRequest - Read a request, after the unit and function code * * @param function - Modbus function code * @param out - Byte stream buffer to hold actual message */ - private void getRequest(int function, BytesOutputStream out) throws IOException { + protected void getRequest(int function, BytesOutputStream out) throws IOException { int byteCount; byte[] inpBuf = new byte[256]; try { @@ -139,7 +163,7 @@ private void getRequest(int function, BytesOutputStream out) throws IOException * @param out The output buffer to put the result * @throws IOException If data cannot be read from the port */ - private void getResponse(int function, BytesOutputStream out) throws IOException { + protected void getResponse(int function, BytesOutputStream out) throws IOException { byte[] inpBuf = new byte[256]; try { if ((function & 0x80) == 0) { @@ -298,7 +322,7 @@ protected ModbusRequest readRequestIn(AbstractModbusListener listener) throws Mo byteInputOutputStream.writeByte(fc); // create request to acquire length of message - request = ModbusRequest.createModbusRequest(fc); + request = createModbusRequest(fc); request.setHeadless(); /* @@ -321,7 +345,7 @@ protected ModbusRequest readRequestIn(AbstractModbusListener listener) throws Mo int[] crc = ModbusUtil.calculateCRC(inBuffer, 0, dlength); // does not include CRC if (ModbusUtil.unsignedByteToInt(inBuffer[dlength]) != crc[0] || ModbusUtil.unsignedByteToInt(inBuffer[dlength + 1]) != crc[1]) { if (logger.isDebugEnabled()) { - logger.debug("CRC should be {}, {} inBuffer={}", Integer.toHexString(crc[0]), Integer.toHexString(crc[1]), + logger.debug("CRC should be {}, {} inBuffer={}", Integer.toHexString(crc[0]), Integer.toHexString(crc[1]), ModbusUtil.toHex(inBuffer, 0, dlength + 2)); } @@ -402,6 +426,10 @@ protected ModbusRequest readRequestIn(AbstractModbusListener listener) throws Mo } } + protected int readUid() throws IOException { + return readByte(); + } + /** * readResponse - Read the bytes for the response from the slave. * @@ -415,21 +443,24 @@ protected ModbusResponse readResponseIn() throws ModbusIOException { ModbusResponse response; int dlength; + int uid = -1; + int fc = -1; + try { do { // 1. read to function code, create request and read function // specific bytes synchronized (byteInputStream) { - int uid = readByte(); + uid = readUid(); if (uid != -1) { - int fc = readByte(); + fc = readByte(); byteInputOutputStream.reset(); byteInputOutputStream.writeByte(uid); byteInputOutputStream.writeByte(fc); // create response to acquire length of message - response = ModbusResponse.createModbusResponse(fc); + response = createModbusResponse(fc); response.setHeadless(); /* @@ -468,7 +499,7 @@ protected ModbusResponse readResponseIn() throws ModbusIOException { } catch (IOException ex) { // FIXME: This printout is wrong when reading response from other slave - throw new ModbusIOException("I/O exception - failed to read response for request [%s] - %s", ModbusUtil.toHex(lastRequest), ex.getMessage()); + throw new ModbusIOException(String.format("I/O exception - failed to read response for request [%s] - uid: %d - fc: %d - %s", ModbusUtil.toHex(lastRequest), uid, fc, ex.getMessage()), ex); } } } diff --git a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java index 317beb8..487ad52 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -426,7 +426,7 @@ void readBytes(byte[] buffer, int bytesToRead) throws IOException { if (commPort != null && commPort.isOpen()) { int cnt = commPort.readBytes(buffer, bytesToRead); if (cnt != bytesToRead) { - throw new IOException("Cannot read from serial port - truncated"); + throw new IOException(String.format("Cannot read from serial port - truncated (expected %d bytes, got %d bytes)", bytesToRead, cnt)); } } else { diff --git a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusTransaction.java b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusTransaction.java index 7fa3a90..2e27c8f 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusTransaction.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusTransaction.java @@ -22,6 +22,7 @@ import com.ghgande.j2mod.modbus.msg.ModbusResponse; import java.util.Random; +import java.util.function.Function; /** * Interface defining a ModbusTransaction. @@ -43,6 +44,7 @@ public abstract class ModbusTransaction { int retries = Modbus.DEFAULT_RETRIES; private final Random random = new Random(System.nanoTime()); static int transactionID = Modbus.DEFAULT_TRANSACTION_ID; + private Function retrySleepTimeCalculatorFunc = (count) -> (Modbus.RETRY_SLEEP_TIME / 2) + (long) (random.nextDouble() * Modbus.RETRY_SLEEP_TIME * count); /** * Returns the ModbusRequest instance @@ -104,6 +106,17 @@ public void setRetries(int retries) { this.retries = retries; } + /** + * Sets the method used for calculating sleep time between retries. + * @param retrySleepTimeCalculatorFunc the argument is the amount of retries, beginning with 1. The return value is the milliseconds to wait. + */ + public void setRetrySleepTimeCalculator(Function retrySleepTimeCalculatorFunc) { + if (retrySleepTimeCalculatorFunc == null) { + throw new IllegalArgumentException("retrySleepTimeCalculatorFunc must not be null"); + } + this.retrySleepTimeCalculatorFunc = retrySleepTimeCalculatorFunc; + } + /** * Tests whether the validity of a transaction * will be checked. @@ -151,8 +164,8 @@ public synchronized int getTransactionID() { * @param count Retry count * @return Random sleep time in milliseconds */ - long getRandomSleepTime(int count) { - return (Modbus.RETRY_SLEEP_TIME / 2) + (long) (random.nextDouble() * Modbus.RETRY_SLEEP_TIME * count); + protected long getRandomSleepTime(int count) { + return this.retrySleepTimeCalculatorFunc.apply(count); } /** diff --git a/src/main/java/com/ghgande/j2mod/modbus/net/AbstractModbusListener.java b/src/main/java/com/ghgande/j2mod/modbus/net/AbstractModbusListener.java index 7de1fab..6854acd 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/net/AbstractModbusListener.java +++ b/src/main/java/com/ghgande/j2mod/modbus/net/AbstractModbusListener.java @@ -144,7 +144,7 @@ public void setTimeout(int timeout) { * @param listener Listener that the request was received by * @throws ModbusIOException If there is an issue with the transport or transmission */ - void handleRequest(AbstractModbusTransport transport, AbstractModbusListener listener) throws ModbusIOException { + protected void handleRequest(AbstractModbusTransport transport, AbstractModbusListener listener) throws ModbusIOException { // Get the request from the transport. It will be processed // using an associated process image diff --git a/src/main/java/com/ghgande/j2mod/modbus/net/SerialConnection.java b/src/main/java/com/ghgande/j2mod/modbus/net/SerialConnection.java index 58e6fb9..097f265 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/net/SerialConnection.java +++ b/src/main/java/com/ghgande/j2mod/modbus/net/SerialConnection.java @@ -103,22 +103,7 @@ public synchronized void open() throws IOException { applyConnectionParameters(); if (!reopen) { - if (Modbus.SERIAL_ENCODING_ASCII.equals(parameters.getEncoding())) { - transport = new ModbusASCIITransport(); - } - else if (Modbus.SERIAL_ENCODING_RTU.equals(parameters.getEncoding())) { - transport = new ModbusRTUTransport(); - } - else { - transport = new ModbusRTUTransport(); - logger.warn("Unknown transport encoding [{}] - reverting to RTU", parameters.getEncoding()); - } - transport.setEcho(parameters.isEcho()); - transport.setTimeout(timeout); - - // Open the input and output streams for the connection. If they won't - // open, close the port before throwing an exception. - transport.setCommPort(this); + this.transport = this.initializeTransport(); } // Open the port so that we can get it's input stream int attempts = 0; @@ -159,6 +144,28 @@ public void serialEvent(SerialPortEvent event) { } } + protected ModbusSerialTransport initializeTransport() throws IOException { + ModbusSerialTransport transport; + if (Modbus.SERIAL_ENCODING_ASCII.equals(parameters.getEncoding())) { + transport = new ModbusASCIITransport(); + } + else if (Modbus.SERIAL_ENCODING_RTU.equals(parameters.getEncoding())) { + transport = new ModbusRTUTransport(); + } + else { + transport = new ModbusRTUTransport(); + logger.warn("Unknown transport encoding [{}] - reverting to RTU", parameters.getEncoding()); + } + transport.setEcho(parameters.isEcho()); + transport.setTimeout(timeout); + + // Open the input and output streams for the connection. If they won't + // open, close the port before throwing an exception. + transport.setCommPort(this); + + return transport; + } + /** * Applies the serial parameters to the actual hardware */ diff --git a/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlave.java b/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlave.java index 43a8a37..214ae69 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlave.java +++ b/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlave.java @@ -17,18 +17,15 @@ import com.ghgande.j2mod.modbus.ModbusException; import com.ghgande.j2mod.modbus.net.AbstractModbusListener; -import com.ghgande.j2mod.modbus.net.ModbusSerialListener; -import com.ghgande.j2mod.modbus.net.ModbusTCPListener; -import com.ghgande.j2mod.modbus.net.ModbusUDPListener; import com.ghgande.j2mod.modbus.procimg.ProcessImage; import com.ghgande.j2mod.modbus.util.ModbusUtil; import com.ghgande.j2mod.modbus.util.SerialParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.InetAddress; import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; /** * Class that implements a wrapper around a Slave Listener @@ -49,98 +46,23 @@ public class ModbusSlave { private final Map processImages = new HashMap(); - /** - * Creates a TCP modbus slave - * - * @param port Port to listen on if IP type - * @param poolSize Pool size for TCP slaves - * @param useRtuOverTcp True if the RTU protocol should be used over TCP - * @throws ModbusException If a problem occurs e.g. port already in use - */ - protected ModbusSlave(int port, int poolSize, boolean useRtuOverTcp) throws ModbusException { - this(ModbusSlaveType.TCP, null, port, poolSize, null, useRtuOverTcp, 0); - } - - /** - * Creates a TCP modbus slave - * - * @param address IP address to listen on - * @param port Port to listen on if IP type - * @param poolSize Pool size for TCP slaves - * @param useRtuOverTcp True if the RTU protocol should be used over TCP - * @throws ModbusException If a problem occurs e.g. port already in use - */ - protected ModbusSlave(InetAddress address, int port, int poolSize, boolean useRtuOverTcp, int maxIdleSeconds) throws ModbusException { - this(ModbusSlaveType.TCP, address, port, poolSize, null, useRtuOverTcp, maxIdleSeconds); - } - - /** - * Creates a UDP modbus slave - * - * @param port Port to listen on if IP type - * @param useRtuOverTcp True if the RTU protocol should be used over TCP - * @throws ModbusException If a problem occurs e.g. port already in use - */ - protected ModbusSlave(int port, boolean useRtuOverTcp) throws ModbusException { - this(ModbusSlaveType.UDP, null, port, 0, null, useRtuOverTcp, 0); - } - - /** - * Creates a UDP modbus slave - * - * @param address IP address to listen on - * @param port Port to listen on if IP type - * @param useRtuOverTcp True if the RTU protocol should be used over TCP - * @throws ModbusException If a problem occurs e.g. port already in use - */ - protected ModbusSlave(InetAddress address, int port, boolean useRtuOverTcp) throws ModbusException { - this(ModbusSlaveType.UDP, address, port, 0, null, useRtuOverTcp, 0); - } - - /** - * Creates a serial modbus slave - * - * @param serialParams Serial parameters for serial type slaves - * @throws ModbusException If a problem occurs e.g. port already in use - */ - protected ModbusSlave(SerialParameters serialParams) throws ModbusException { - this(ModbusSlaveType.SERIAL, null, 0, 0, serialParams, false, 0); - } - /** * Creates an appropriate type of listener * - * @param type Type of slave to create - * @param address IP address to listen on - * @param port Port to listen on if IP type - * @param poolSize Pool size for TCP slaves - * @param serialParams Serial parameters for serial type slaves - * @param useRtuOverTcp True if the RTU protocol should be used over TCP - * @param maxIdleSeconds Maximum idle seconds for TCP connection + * @param type Type of slave to create + * @param serialParams Serial parameters for serial type slaves + * @param listenerFactory method used to create the modbus listener */ - private ModbusSlave(ModbusSlaveType type, InetAddress address, int port, int poolSize, SerialParameters serialParams, boolean useRtuOverTcp, int maxIdleSeconds) { + protected ModbusSlave(ModbusSlaveType type, SerialParameters serialParams, Supplier listenerFactory) { this.type = type == null ? ModbusSlaveType.TCP : type; - this.port = port; this.serialParams = serialParams; // Create the listener - logger.debug("Creating {} listener", this.type); - if (this.type.is(ModbusSlaveType.UDP)) { - listener = new ModbusUDPListener(); - } - else if (this.type.is(ModbusSlaveType.TCP)) { - ModbusTCPListener tcpListener = new ModbusTCPListener(poolSize, useRtuOverTcp); - tcpListener.setMaxIdleSeconds(maxIdleSeconds); - listener = tcpListener; - } - else { - listener = new ModbusSerialListener(serialParams); - } + this.listener = listenerFactory.get(); + this.listener.setTimeout(0); - listener.setAddress(address); - listener.setPort(port); - listener.setTimeout(0); + this.port = this.listener.getPort(); } /** diff --git a/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlaveFactory.java b/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlaveFactory.java index 381fb98..b6d02b8 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlaveFactory.java +++ b/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlaveFactory.java @@ -17,6 +17,9 @@ import com.ghgande.j2mod.modbus.ModbusException; import com.ghgande.j2mod.modbus.net.AbstractModbusListener; +import com.ghgande.j2mod.modbus.net.ModbusSerialListener; +import com.ghgande.j2mod.modbus.net.ModbusTCPListener; +import com.ghgande.j2mod.modbus.net.ModbusUDPListener; import com.ghgande.j2mod.modbus.util.ModbusUtil; import com.ghgande.j2mod.modbus.util.SerialParameters; @@ -24,6 +27,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; /** * This is a factory class that allows users to easily create and manage slaves.
@@ -98,7 +102,13 @@ public static synchronized ModbusSlave createTCPSlave(InetAddress address, int p return slaves.get(key); } else { - ModbusSlave slave = new ModbusSlave(address, port, poolSize, useRtuOverTcp, maxIdleSeconds); + ModbusSlave slave = new ModbusSlave(ModbusSlaveType.TCP, null, () -> { + ModbusTCPListener listener = new ModbusTCPListener(poolSize, useRtuOverTcp); + listener.setAddress(address); + listener.setPort(port); + listener.setMaxIdleSeconds(maxIdleSeconds); + return listener; + }); slaves.put(key, slave); return slave; } @@ -129,7 +139,12 @@ public static synchronized ModbusSlave createUDPSlave(InetAddress address, int p return slaves.get(key); } else { - ModbusSlave slave = new ModbusSlave(address, port, false); + ModbusSlave slave = new ModbusSlave(ModbusSlaveType.UDP, null, () -> { + ModbusUDPListener listener = new ModbusUDPListener(); + listener.setAddress(address); + listener.setPort(port); + return listener; + }); slaves.put(key, slave); return slave; } @@ -143,6 +158,17 @@ public static synchronized ModbusSlave createUDPSlave(InetAddress address, int p * @throws ModbusException If a problem occurs e.g. port already in use */ public static synchronized ModbusSlave createSerialSlave(SerialParameters serialParams) throws ModbusException { + return createSerialSlave(serialParams, () -> new ModbusSerialListener(serialParams)); + } + + /** + * Creates a serial modbus slave or returns the one already allocated to this port + * + * @param serialParams Serial parameters for serial type slaves + * @return new or existing Serial modbus slave associated with the port + * @throws ModbusException If a problem occurs e.g. port already in use + */ + public static synchronized ModbusSlave createSerialSlave(SerialParameters serialParams, Supplier listenerFactory) throws ModbusException { ModbusSlave slave; if (serialParams == null) { throw new ModbusException("Serial parameters are null"); @@ -163,7 +189,7 @@ else if (ModbusUtil.isBlank(serialParams.getPortName())) { // If we don;t have a slave, create one if (slave == null) { - slave = new ModbusSlave(serialParams); + slave = new ModbusSlave(ModbusSlaveType.SERIAL, serialParams, listenerFactory); slaves.put(ModbusSlaveType.SERIAL.getKey(serialParams.getPortName()), slave); } return slave;