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 59c4b4a3..b12b8cb9 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusRTUTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusRTUTransport.java @@ -371,9 +371,9 @@ protected ModbusRequest readRequestIn(AbstractModbusListener listener) throws Mo if (!bytesAvailable) { // Sleep the 1.5t to see if there will be more data if (logger.isDebugEnabled()) { - logger.debug("Waiting for {} microsec", getMaxCharDelay()); + logger.debug("Waiting for {} microsec", getMaxCharTimeout()); } - bytesAvailable = spinUntilBytesAvailable(getMaxCharDelay()); + bytesAvailable = spinUntilBytesAvailable(getMaxCharTimeout()); } if (bytesAvailable) { 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 487ad52b..7c1ccc6a 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -15,7 +15,6 @@ */ package com.ghgande.j2mod.modbus.io; -import com.fazecast.jSerialComm.SerialPort; import com.ghgande.j2mod.modbus.Modbus; import com.ghgande.j2mod.modbus.ModbusIOException; import com.ghgande.j2mod.modbus.msg.ModbusMessage; @@ -55,11 +54,42 @@ public abstract class ModbusSerialTransport extends AbstractModbusTransport { static final int FRAME_END = 2000; /** - * The number of nanoseconds there is in a millisecond + * The number of nanoseconds in a millisecond */ - private static final int NS_IN_A_MS = 1000000; + private static final double NS_IN_A_MS = 1_000_000.0; + + /** + * The number of microseconds in a second. + */ + private static final double MICROS_IN_A_SEC = 1_000_000.0; + + /** + * The number of nanoseconds in a second + */ + private static final double NS_IN_A_SEC = 1_000_000_000.0; + private static final String CANNOT_READ_FROM_SERIAL_PORT = "Cannot read from serial port"; private static final String COMM_PORT_IS_NOT_VALID_OR_NOT_OPEN = "Comm port is not valid or not open"; + + + /** + * Minimum sleep duration in nanoseconds. + * Below this, only busy-waiting is accurate. + */ + private static final long SLEEP_MIN_NS = 1_000_000L; + + /** + * Safety buffer subtracted from sleep time + * so the thread wakes up early and finishes precision timing via busy-wait. + */ + private static final long SLEEP_MARGIN_NS = 750_000L; + + /** + * Historical calibration factors, for Transmission wait timing. + */ + private static final double LONG_DELAY_FUDGE_FACTOR = 1.7; + private static final double SHORT_DELAY_FUDGE_FACTOR = 1.3; + private AbstractSerialConnection commPort; boolean echo = false; // require RS-485 echo processing private final Set listeners = Collections.synchronizedSet(new HashSet()); @@ -96,6 +126,42 @@ public void writeRequest(ModbusRequest msg) throws ModbusIOException { writeMessage(msg); } + private void waitForTransmission(double transmissionTimeNanos) { + if (transmissionTimeNanos <= 0) { + return; + } + + final double fudgeFactor = (transmissionTimeNanos >= NS_IN_A_MS) // + ? LONG_DELAY_FUDGE_FACTOR // + : SHORT_DELAY_FUDGE_FACTOR; + final long targetEndNanos = System.nanoTime() + (long) (transmissionTimeNanos * fudgeFactor); + + try { + long remainingNanos = targetEndNanos - System.nanoTime(); + if (remainingNanos >= (SLEEP_MIN_NS + SLEEP_MARGIN_NS)) { + long sleepMillis = (long) ((remainingNanos - SLEEP_MARGIN_NS) / NS_IN_A_MS); + Thread.sleep(sleepMillis); + } + if (transmissionTimeNanos >= 5 * NS_IN_A_MS) { + // For long delays, allow the scheduler to run other threads + // before entering the final high-precision spin phase. + while ((targetEndNanos - System.nanoTime()) > 100_000L) { + Thread.sleep(0); + } + } + while (System.nanoTime() < targetEndNanos) { + // Pure busy wait + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.debug("waitForTransmission interrupted. Ignoring.", e); + } + catch (RuntimeException ex) { + logger.debug("waitForTransmission failed with exception. Ignoring.", ex); + } + } + /** * Writes the request/response message to the port * @@ -107,35 +173,11 @@ private void writeMessage(ModbusMessage msg) throws ModbusIOException { notifyListenersBeforeWrite(msg); try { writeMessageOut(msg); - long startTime = System.nanoTime(); // Wait here for the message to have been sent - - double bytesPerSec = ((double)commPort.getBaudRate()) / (((commPort.getNumDataBits() == 0) ? 8 : commPort.getNumDataBits()) + ((commPort.getNumStopBits() == 0) ? 1 : commPort.getNumStopBits()) + ((commPort.getParity() == SerialPort.NO_PARITY) ? 0 : 1)); - double delay = 1000000000.0 * msg.getOutputLength() / bytesPerSec; - double delayMilliSeconds = Math.floor(delay / 1000000); - double delayNanoSeconds = delay % 1000000; - try { - - // For delays less than a millisecond, we need to chew CPU cycles unfortunately - // There are some fiddle factors here to allow for some oddities in the hardware - - if (delayMilliSeconds == 0.0) { - int priority = Thread.currentThread().getPriority(); - Thread.currentThread().setPriority(Thread.MIN_PRIORITY); - long end = startTime + ((int) (delayNanoSeconds * 1.3)); - while (System.nanoTime() < end) { - // noop - } - Thread.currentThread().setPriority(priority); - } - else { - Thread.sleep((int) (delayMilliSeconds * 1.7), (int) (delayNanoSeconds * 1.5)); - } - } - catch (Exception e) { - logger.debug("nothing to do"); - } + final double charactersPerSecond = commPort.getBaudRate() / commPort.getBitsPerCharacter(); + final double transmissionTimeNanos = (msg.getOutputLength() / charactersPerSecond) * NS_IN_A_SEC; + waitForTransmission(transmissionTimeNanos); } finally { notifyListenersAfterWrite(msg); @@ -200,7 +242,6 @@ public void setTimeout(int time) { * * @param listener Listener that received this request * @return a ModbusRequest value - * * @throws ModbusIOException if an error occurs */ protected abstract ModbusRequest readRequestIn(AbstractModbusListener listener) throws ModbusIOException; @@ -210,7 +251,6 @@ public void setTimeout(int time) { * responding to a master writeRequest request. * * @return a ModbusResponse value - * * @throws ModbusIOException if an error occurs */ protected abstract ModbusResponse readResponseIn() throws ModbusIOException; @@ -328,7 +368,7 @@ public void notifyListenersDisconnected() { } } } - + /** * setCommPort sets the comm port member and prepares the input * and output streams to be used for reading from and writing to. @@ -396,7 +436,6 @@ protected int availableBytes() { * Reads a byte from the comms port * * @return Value of the byte - * * @throws IOException If it cannot read or times out */ protected int readByte() throws IOException { @@ -440,7 +479,6 @@ void readBytes(byte[] buffer, int bytesToRead) throws IOException { * @param buffer Buffer to write * @param bytesToWrite Number of bytes to write * @return Number of bytes written - * * @throws java.io.IOException if writing to invalid port */ final int writeBytes(byte[] buffer, int bytesToWrite) throws IOException { @@ -457,7 +495,6 @@ final int writeBytes(byte[] buffer, int bytesToWrite) throws IOException { * It handles the special start and end frame markers * * @return Byte value of the next ASCII couplet - * * @throws IOException If a problem with the port */ int readAsciiByte() throws IOException { @@ -503,7 +540,6 @@ else if (buffer[0] == '\r' || buffer[0] == '\n') { * * @param value Value to write * @return Number of bytes written - * * @throws IOException If a problem with the port */ final int writeAsciiByte(int value) throws IOException { @@ -542,7 +578,6 @@ else if (value == FRAME_END) { * @param buffer Buffer of bytes to write * @param bytesToWrite Number of characters to write * @return Number of bytes written - * * @throws IOException If a problem with the port */ int writeAsciiBytes(byte[] buffer, long bytesToWrite) throws IOException { @@ -614,7 +649,7 @@ void waitBetweenFrames(int transDelayMS, long lastTransactionTimestamp) { int delay = getInterFrameDelay() / 1000; // How long since the last message we received - long gapSinceLastMessage = (System.nanoTime() - lastTransactionTimestamp) / NS_IN_A_MS; + final long gapSinceLastMessage = (long) ((System.nanoTime() - lastTransactionTimestamp) / NS_IN_A_MS); if (delay > gapSinceLastMessage) { long sleepTime = delay - gapSinceLastMessage; @@ -628,9 +663,14 @@ void waitBetweenFrames(int transDelayMS, long lastTransactionTimestamp) { } /** - * In microseconds + * Calculates the inter-frame delay according to the + * MODBUS over Serial Line Specification V1.02. + * * - * @return Delay between frames + * @return the inter-frame delay in microseconds */ int getInterFrameDelay() { if (commPort.getBaudRate() > 19200) { @@ -638,18 +678,23 @@ int getInterFrameDelay() { } else { long delay = Math.max(getCharIntervalMicro(Modbus.INTER_MESSAGE_GAP), Modbus.MINIMUM_TRANSMIT_DELAY * 1000L); - return delay > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) delay; + return (int) Math.min(Integer.MAX_VALUE, delay); } } /** - * The maximum delay between characters in microseconds + * Calculates the inter-character time-out according to the + * MODBUS over Serial Line Specification V1.02. + * * - * @return microseconds + * @return the inter-character time-out in microseconds */ - long getMaxCharDelay() { + long getMaxCharTimeout() { if (commPort.getBaudRate() > 19200) { - return 1750; + return 750; } else { return getCharIntervalMicro(Modbus.INTER_CHARACTER_GAP); @@ -667,7 +712,8 @@ long getCharIntervalMicro(double chars) { // Make use we have a gap of 3.5 characters between adjacent requests // We have to do the calculations here because it is possible that the caller may have changed // the connection characteristics if they provided the connection instance - return (long) chars * NS_IN_A_MS * (1 + commPort.getNumDataBits() + commPort.getNumStopBits() + (commPort.getParity() == AbstractSerialConnection.NO_PARITY ? 0 : 1)) / commPort.getBaudRate(); + final double microsPerChar = (commPort.getBitsPerCharacter() / (double) commPort.getBaudRate()) * MICROS_IN_A_SEC; + return (long) (microsPerChar * chars); } /** diff --git a/src/main/java/com/ghgande/j2mod/modbus/net/AbstractSerialConnection.java b/src/main/java/com/ghgande/j2mod/modbus/net/AbstractSerialConnection.java index 7d12aaec..be0ff4b1 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/net/AbstractSerialConnection.java +++ b/src/main/java/com/ghgande/j2mod/modbus/net/AbstractSerialConnection.java @@ -101,9 +101,11 @@ public abstract class AbstractSerialConnection { public abstract void close(); /** - * Returns current baud rate + * Returns current baud rate. + *

+ * For UART interfaces (RS-232 / RS-485), this is equal to the line bit rate in bits/s. * - * @return Baud rate + * @return Baud rate (bits/s) */ public abstract int getBaudRate(); @@ -115,12 +117,34 @@ public abstract class AbstractSerialConnection { public abstract int getNumDataBits(); /** - * Returns current stop bits + * Returns current stop bits configuration constant. + *

+ * Use {@link #getStopBits()} to get the actual stop bits in bit times. * - * @return Number of stop bits + * @return Stop-bit configuration constant. */ public abstract int getNumStopBits(); + /** + * Returns current stop bits as actual bit times. + *

+ * Use {@link #getNumStopBits()} to get the stop bits configuration constant. + * + * @return Stop-bit length in bit times. + */ + public float getStopBits() { + switch (getNumStopBits()) { + case ONE_STOP_BIT: + return 1.0f; + case ONE_POINT_FIVE_STOP_BITS: + return 1.5f; + case TWO_STOP_BITS: + return 2.0f; + default: + return 1.0f; + } + } + /** * Returns current parity * @@ -179,4 +203,20 @@ public abstract class AbstractSerialConnection { */ public abstract Set getCommPorts(); + /** + * Returns the total number of serial bit-times required to transmit + * a single character with the current port configuration. + * + * @return Total bit-times per character. + */ + public double getBitsPerCharacter() { + final double startBit = 1.0; + final int numDataBits = getNumDataBits(); + final int dataBits = numDataBits == 0 ? 8 : numDataBits; + final double stopBits = getStopBits(); + final double parityBits = getParity() == NO_PARITY ? 0 : 1; + + return startBit + dataBits + stopBits + parityBits; + } + } 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 b6d02b8d..fc43cdf5 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlaveFactory.java +++ b/src/main/java/com/ghgande/j2mod/modbus/slave/ModbusSlaveFactory.java @@ -165,6 +165,7 @@ public static synchronized ModbusSlave createSerialSlave(SerialParameters serial * Creates a serial modbus slave or returns the one already allocated to this port * * @param serialParams Serial parameters for serial type slaves + * @param listenerFactory Factory to create the listener for this slave * @return new or existing Serial modbus slave associated with the port * @throws ModbusException If a problem occurs e.g. port already in use */ diff --git a/src/main/java/com/ghgande/j2mod/modbus/util/SerialParameters.java b/src/main/java/com/ghgande/j2mod/modbus/util/SerialParameters.java index ca5601ac..d96ea123 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/util/SerialParameters.java +++ b/src/main/java/com/ghgande/j2mod/modbus/util/SerialParameters.java @@ -37,7 +37,7 @@ public class SerialParameters { private static final boolean DEFAULT_RS485_MODE = false; private static final boolean DEFAULT_RS485_TX_ENABLE_ACTIVE_HIGH = true; private static final boolean DEFAULT_RS485_ENABLE_TERMINATION = false; - private static final boolean DEFAULT_RS485_TX_DURING_RX = false; + private static final boolean DEFAULT_RS485_RX_DURING_TX = false; private static final int DEFAULT_RS485_DELAY_BEFORE_TX_MICROSECONDS = 1000; private static final int DEFAULT_RS485_DELAY_AFTER_TX_MICROSECONDS = 1000; @@ -65,24 +65,13 @@ public class SerialParameters { * default values. */ public SerialParameters() { - portName = ""; - baudRate = 9600; - flowControlIn = AbstractSerialConnection.FLOW_CONTROL_DISABLED; - flowControlOut = AbstractSerialConnection.FLOW_CONTROL_DISABLED; - databits = 8; - stopbits = AbstractSerialConnection.ONE_STOP_BIT; - parity = AbstractSerialConnection.NO_PARITY; - // Historically, the encoding has been null which got converted to RTU - // by SerialConnection.open(). Let's make it more explicit which serial - // protocol will be used by default. - encoding = Modbus.SERIAL_ENCODING_RTU; - echo = false; - openDelay = AbstractSerialConnection.OPEN_DELAY; - rs485Mode = DEFAULT_RS485_MODE; - rs485TxEnableActiveHigh = DEFAULT_RS485_TX_ENABLE_ACTIVE_HIGH; - rs485DelayBeforeTxMicroseconds = DEFAULT_RS485_DELAY_BEFORE_TX_MICROSECONDS; - rs485DelayAfterTxMicroseconds = DEFAULT_RS485_DELAY_AFTER_TX_MICROSECONDS; - rs485DisableControl = DEFAULT_RS485_DISABLE_CONTROL; + this("", 9600, + AbstractSerialConnection.FLOW_CONTROL_DISABLED, + AbstractSerialConnection.FLOW_CONTROL_DISABLED, + 8, + AbstractSerialConnection.ONE_STOP_BIT, + AbstractSerialConnection.NO_PARITY, + false); } /** @@ -105,17 +94,17 @@ public SerialParameters(String portName, int baudRate, int stopbits, int parity, boolean echo) { - // Perform default initialization and update fields of interest - // afterwards. - this(); - this.portName = portName; - this.baudRate = baudRate; - this.flowControlIn = flowControlIn; - this.flowControlOut = flowControlOut; - this.databits = databits; - this.stopbits = stopbits; - this.parity = parity; - this.echo = echo; + this(portName, baudRate, + flowControlIn, + flowControlOut, + databits, + stopbits, + parity, + echo, + DEFAULT_RS485_MODE, + DEFAULT_RS485_TX_ENABLE_ACTIVE_HIGH, + DEFAULT_RS485_DELAY_BEFORE_TX_MICROSECONDS, + DEFAULT_RS485_DELAY_AFTER_TX_MICROSECONDS); } /** @@ -160,13 +149,26 @@ public SerialParameters(String portName, int baudRate, int rs485DelayBeforeTxMicroseconds, int rs485DelayAfterTxMicroseconds ) { - // Perform default non-RS-485 initialization and update fields of - // interest afterwards. - this(portName, baudRate, flowControlIn, flowControlOut, databits, stopbits, parity, echo); + this.portName = portName; + this.setBaudRate(baudRate); + this.flowControlIn = flowControlIn; + this.flowControlOut = flowControlOut; + this.databits = databits; + this.stopbits = stopbits; + this.parity = parity; + this.echo = echo; this.rs485Mode = rs485Mode; this.rs485TxEnableActiveHigh = rs485TxEnableActiveHigh; this.rs485DelayBeforeTxMicroseconds = rs485DelayBeforeTxMicroseconds; this.rs485DelayAfterTxMicroseconds = rs485DelayAfterTxMicroseconds; + // Historically, the encoding has been null which got converted to RTU + // by SerialConnection.open(). Let's make it more explicit which serial + // protocol will be used by default. + this.encoding = Modbus.SERIAL_ENCODING_RTU; + this.openDelay = AbstractSerialConnection.OPEN_DELAY; + this.rs485DisableControl = DEFAULT_RS485_DISABLE_CONTROL; + this.rs485EnableTermination = DEFAULT_RS485_ENABLE_TERMINATION; + this.rs485RxDuringTx = DEFAULT_RS485_RX_DURING_TX; } /** @@ -217,30 +219,35 @@ public void setPortName(String name) { } /** - * Sets the baud rate. + * Sets the baud rate. Has to be ≥1. * * @param rate the new baud rate. */ public void setBaudRate(int rate) { + if (rate < 1) { + throw new IllegalArgumentException("Baud rate must be greater than 0, but was: " + rate); + } baudRate = rate; } /** - * Return the baud rate as int. + * Sets the baud rate. * - * @return the baud rate as int. + * @param rate the new baud rate. */ - public int getBaudRate() { - return baudRate; + public void setBaudRate(String rate) { + setBaudRate(Integer.parseInt(rate)); } /** - * Sets the baud rate. + * Return the baud rate as int. + *

+ * Is guaranteed to return a value ≥1 * - * @param rate the new baud rate. + * @return the baud rate as int. */ - public void setBaudRate(String rate) { - baudRate = Integer.parseInt(rate); + public int getBaudRate() { + return baudRate; } /** diff --git a/src/test/java/com/ghgande/j2mod/modbus/net/SerialConnectionTest.java b/src/test/java/com/ghgande/j2mod/modbus/net/SerialConnectionTest.java new file mode 100644 index 00000000..a167128a --- /dev/null +++ b/src/test/java/com/ghgande/j2mod/modbus/net/SerialConnectionTest.java @@ -0,0 +1,85 @@ +package com.ghgande.j2mod.modbus.net; + +import com.ghgande.j2mod.modbus.util.SerialParameters; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SerialConnectionTest { + + @Test + public void testBitsPerCharacter_Standard8N1() { + SerialParameters parameters = new SerialParameters("", 9600, + AbstractSerialConnection.FLOW_CONTROL_DISABLED, + AbstractSerialConnection.FLOW_CONTROL_DISABLED, + 8, + AbstractSerialConnection.ONE_STOP_BIT, + AbstractSerialConnection.NO_PARITY, + false + ); + SerialConnection serialCon = new SerialConnection(parameters); + + // 1 Startbit + 8 Datenbits + 0 Parität + 1 Stoppbit = 10 + assertEquals(10.0, serialCon.getBitsPerCharacter(), 0.001); + } + + @Test + public void testBitsPerCharacter_FiveBitsAndOnePointFiveStopBits() { + SerialParameters parameters = new SerialParameters("", 9600, + AbstractSerialConnection.FLOW_CONTROL_DISABLED, + AbstractSerialConnection.FLOW_CONTROL_DISABLED, + 5, + AbstractSerialConnection.ONE_POINT_FIVE_STOP_BITS, + AbstractSerialConnection.NO_PARITY, + false + ); + SerialConnection serialCon = new SerialConnection(parameters); + + // 1 Startbit + 5 Datenbits + 0 Parität + 1.5 Stoppbit = 7.5 + assertEquals(7.5, serialCon.getBitsPerCharacter(), 0.001); + } + + @Test + public void testBitsPerCharacter_WithParityAndTwoStopBits() { + SerialParameters parameters = new SerialParameters("", 9600, + AbstractSerialConnection.FLOW_CONTROL_DISABLED, + AbstractSerialConnection.FLOW_CONTROL_DISABLED, + 8, + AbstractSerialConnection.TWO_STOP_BITS, + AbstractSerialConnection.MARK_PARITY, + false + ); + SerialConnection serialCon = new SerialConnection(parameters); + + // 1 Startbit + 8 Datenbits + 1 Parität + 2 Stoppbits = 12 + assertEquals(12.0, serialCon.getBitsPerCharacter(), 0.001); + } + + @Test + public void testStopBits_OneStopBits() { + SerialParameters parameters = new SerialParameters(); + parameters.setStopbits(AbstractSerialConnection.ONE_STOP_BIT); + + SerialConnection serialCon = new SerialConnection(parameters); + assertEquals(1.0, serialCon.getStopBits(), 0.001); + } + + @Test + public void testStopBits_OnePointFiveStopBits() { + SerialParameters parameters = new SerialParameters(); + parameters.setStopbits(AbstractSerialConnection.ONE_POINT_FIVE_STOP_BITS); + + SerialConnection serialCon = new SerialConnection(parameters); + assertEquals(1.5, serialCon.getStopBits(), 0.001); + } + + @Test + public void testStopBits_TwoStopBits() { + SerialParameters parameters = new SerialParameters(); + parameters.setStopbits(AbstractSerialConnection.TWO_STOP_BITS); + + SerialConnection serialCon = new SerialConnection(parameters); + assertEquals(2.0, serialCon.getStopBits(), 0.001); + } + +} diff --git a/src/test/java/com/ghgande/j2mod/modbus/util/SerialParametersTest.java b/src/test/java/com/ghgande/j2mod/modbus/util/SerialParametersTest.java index 5e2f92a0..3cf61c79 100644 --- a/src/test/java/com/ghgande/j2mod/modbus/util/SerialParametersTest.java +++ b/src/test/java/com/ghgande/j2mod/modbus/util/SerialParametersTest.java @@ -5,6 +5,7 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; public class SerialParametersTest { @@ -172,4 +173,13 @@ public void testSetFlowControlOut() { SerialParameters serialParameters = new SerialParameters(); serialParameters.setFlowControlOut(-1); } + + @Test + public void testInvalidBaudRatesThrowException() { + SerialParameters serialParameters = new SerialParameters(); + assertThrows(IllegalArgumentException.class, () -> serialParameters.setBaudRate(0)); + assertThrows(IllegalArgumentException.class, () -> serialParameters.setBaudRate(-1)); + assertThrows(IllegalArgumentException.class, () -> serialParameters.setBaudRate("0")); + assertThrows(IllegalArgumentException.class, () -> serialParameters.setBaudRate("-1")); + } } \ No newline at end of file