From 8f1141b4906f167bec682d81b4d10382bd6b4090 Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Wed, 29 Jul 2026 18:01:46 +0200 Subject: [PATCH 01/17] fix make shure milli and nano values are in range --- .../modbus/io/ModbusSerialTransport.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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..9fbe8847 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -113,24 +113,27 @@ private void writeMessage(ModbusMessage msg) throws ModbusIOException { 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; + double delayMilliSeconds = Math.max(0.0, Math.floor(delay / 1000000)); + double delayNanoSeconds = Math.max(0.0, 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 + if (delayNanoSeconds > 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); } - Thread.currentThread().setPriority(priority); } else { - Thread.sleep((int) (delayMilliSeconds * 1.7), (int) (delayNanoSeconds * 1.5)); + final int nanosSleep = Math.max(999999, (int) (delayNanoSeconds * 1.5)); + Thread.sleep((int) (delayMilliSeconds * 1.7), nanosSleep); } } catch (Exception e) { From c352e3c83c55b413d3d0af3c9ee965084601246c Mon Sep 17 00:00:00 2001 From: da Kai Date: Fri, 31 Jul 2026 09:00:21 +0200 Subject: [PATCH 02/17] quickfix --- .../java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9fbe8847..1dc105e1 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -132,7 +132,7 @@ private void writeMessage(ModbusMessage msg) throws ModbusIOException { } } else { - final int nanosSleep = Math.max(999999, (int) (delayNanoSeconds * 1.5)); + final int nanosSleep = Math.min(999999, (int) (delayNanoSeconds * 1.5)); Thread.sleep((int) (delayMilliSeconds * 1.7), nanosSleep); } } From b80293401720f03a67a03e6315c69a9894837047 Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Wed, 5 Aug 2026 17:21:09 +0200 Subject: [PATCH 03/17] improve --- .../modbus/io/ModbusSerialTransport.java | 79 ++++++++++++------- 1 file changed, 51 insertions(+), 28 deletions(-) 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 1dc105e1..766b7e8c 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -31,6 +31,8 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; /** * Abstract base class for serial ModbusTransport @@ -96,6 +98,11 @@ public void writeRequest(ModbusRequest msg) throws ModbusIOException { writeMessage(msg); } + // Historical calibration factors. + private static final double MILLIS_SLEEP_FUDGE_FACTOR = 1.7; + private static final double NANOS_SLEEP_FUDGE_FACTOR_S = 1.3; + private static final double NANOS_SLEEP_FUDGE_FACTOR_L = 1.5; + /** * Writes the request/response message to the port * @@ -110,39 +117,55 @@ private void writeMessage(ModbusMessage msg) throws ModbusIOException { long startTime = System.nanoTime(); // Wait here for the message to have been sent + double bytesPerSec = getBytesPerSecond(); + double transmissionTimeNanos = 1_000_000_000.0 * msg.getOutputLength() / bytesPerSec; + waitForTransmission(startTime, transmissionTimeNanos); + } + finally { + notifyListenersAfterWrite(msg); + } + } - 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.max(0.0, Math.floor(delay / 1000000)); - double delayNanoSeconds = Math.max(0.0, delay % 1000000); + private double getBytesPerSecond() { + final double baudRate = commPort.getBaudRate(); + final double startBit = 1.0; + final double dataBits = commPort.getNumDataBits() == 0 ? 8 : commPort.getNumDataBits(); + final double stopBits = commPort.getNumStopBits() == 0 ? 1 : commPort.getNumStopBits(); + final double parityBits = commPort.getParity() == SerialPort.NO_PARITY ? 0 : 1; + + return Math.max(10.0, baudRate / ( startBit + dataBits + stopBits + parityBits )); + } + + private void waitForTransmission(long startTime, double transmissionTimeNanos) { + final long sleepMillis = (long) Math.floor(transmissionTimeNanos / 1_000_000.0); + final double sleepNanos = transmissionTimeNanos % 1_000_000; + + if (sleepMillis > 0) { try { + final double fudgedNanoSleep = sleepNanos * NANOS_SLEEP_FUDGE_FACTOR_L; - // 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) { - if (delayNanoSeconds > 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 { - final int nanosSleep = Math.min(999999, (int) (delayNanoSeconds * 1.5)); - Thread.sleep((int) (delayMilliSeconds * 1.7), nanosSleep); - } + final long totalSleepMillis = (long) ((sleepMillis * MILLIS_SLEEP_FUDGE_FACTOR) + (fudgedNanoSleep / 1_000_000.0)); + final int totalSleepNanos = (int) fudgedNanoSleep % 1_000_000; + + Thread.sleep(totalSleepMillis, totalSleepNanos); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.debug("nothing to do. Sleep interrupted.", e); } - catch (Exception e) { - logger.debug("nothing to do"); + } else if (sleepNanos > 0) { + // 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 + final int priority = Thread.currentThread().getPriority(); + try { + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + long end = startTime + (long) (sleepNanos * NANOS_SLEEP_FUDGE_FACTOR_S); + while (System.nanoTime() < end) { + // noop + } + } finally { + Thread.currentThread().setPriority(priority); } } - finally { - notifyListenersAfterWrite(msg); - } } @Override @@ -331,7 +354,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. From 4fefda0a68530759a732cdfa993d6d657315fd23 Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Tue, 11 Aug 2026 12:34:46 +0200 Subject: [PATCH 04/17] use NS_IN_A_MS --- .../modbus/io/ModbusSerialTransport.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) 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 766b7e8c..96f4249d 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -59,9 +59,17 @@ public abstract class ModbusSerialTransport extends AbstractModbusTransport { /** * The number of nanoseconds there is in a millisecond */ - private static final int NS_IN_A_MS = 1000000; + private static final int NS_IN_A_MS = 1_000_000; 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"; + + /** + * Historical calibration factors, for Transmission wait timing. + */ + private static final double MILLIS_SLEEP_FUDGE_FACTOR = 1.7; + private static final double NANOS_SLEEP_FUDGE_FACTOR_SHORT = 1.3; + private static final double NANOS_SLEEP_FUDGE_FACTOR_LONG = 1.5; + private AbstractSerialConnection commPort; boolean echo = false; // require RS-485 echo processing private final Set listeners = Collections.synchronizedSet(new HashSet()); @@ -98,11 +106,6 @@ public void writeRequest(ModbusRequest msg) throws ModbusIOException { writeMessage(msg); } - // Historical calibration factors. - private static final double MILLIS_SLEEP_FUDGE_FACTOR = 1.7; - private static final double NANOS_SLEEP_FUDGE_FACTOR_S = 1.3; - private static final double NANOS_SLEEP_FUDGE_FACTOR_L = 1.5; - /** * Writes the request/response message to the port * @@ -137,15 +140,15 @@ private double getBytesPerSecond() { } private void waitForTransmission(long startTime, double transmissionTimeNanos) { - final long sleepMillis = (long) Math.floor(transmissionTimeNanos / 1_000_000.0); - final double sleepNanos = transmissionTimeNanos % 1_000_000; + final long sleepMillis = (long) Math.floor(transmissionTimeNanos / NS_IN_A_MS); + final double sleepNanos = transmissionTimeNanos % NS_IN_A_MS; if (sleepMillis > 0) { try { - final double fudgedNanoSleep = sleepNanos * NANOS_SLEEP_FUDGE_FACTOR_L; + final double fudgedNanoSleep = sleepNanos * NANOS_SLEEP_FUDGE_FACTOR_LONG; - final long totalSleepMillis = (long) ((sleepMillis * MILLIS_SLEEP_FUDGE_FACTOR) + (fudgedNanoSleep / 1_000_000.0)); - final int totalSleepNanos = (int) fudgedNanoSleep % 1_000_000; + final long totalSleepMillis = (long) ((sleepMillis * MILLIS_SLEEP_FUDGE_FACTOR) + (fudgedNanoSleep / NS_IN_A_MS)); + final int totalSleepNanos = (int) fudgedNanoSleep % NS_IN_A_MS; Thread.sleep(totalSleepMillis, totalSleepNanos); } catch (InterruptedException e) { @@ -158,7 +161,7 @@ private void waitForTransmission(long startTime, double transmissionTimeNanos) { final int priority = Thread.currentThread().getPriority(); try { Thread.currentThread().setPriority(Thread.MIN_PRIORITY); - long end = startTime + (long) (sleepNanos * NANOS_SLEEP_FUDGE_FACTOR_S); + long end = startTime + (long) (sleepNanos * NANOS_SLEEP_FUDGE_FACTOR_SHORT); while (System.nanoTime() < end) { // noop } From bd12151d8da554ec234084f0365f921855fd9c01 Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Tue, 11 Aug 2026 12:35:33 +0200 Subject: [PATCH 05/17] Update ModbusSerialTransport.java --- .../java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java | 2 -- 1 file changed, 2 deletions(-) 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 96f4249d..8b037657 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -31,8 +31,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; /** * Abstract base class for serial ModbusTransport From d1f89d53bbd7ba9971d3e77696d19ca58276959a Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Tue, 11 Aug 2026 14:56:11 +0200 Subject: [PATCH 06/17] further improve --- .../modbus/io/ModbusSerialTransport.java | 89 +++++++++---------- 1 file changed, 43 insertions(+), 46 deletions(-) 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 8b037657..8cf122e9 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -62,11 +62,10 @@ public abstract class ModbusSerialTransport extends AbstractModbusTransport { private static final String COMM_PORT_IS_NOT_VALID_OR_NOT_OPEN = "Comm port is not valid or not open"; /** - * Historical calibration factors, for Transmission wait timing. + * Historical calibration factors, for Transmission wait timing. */ - private static final double MILLIS_SLEEP_FUDGE_FACTOR = 1.7; - private static final double NANOS_SLEEP_FUDGE_FACTOR_SHORT = 1.3; - private static final double NANOS_SLEEP_FUDGE_FACTOR_LONG = 1.5; + 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 @@ -105,70 +104,75 @@ public void writeRequest(ModbusRequest msg) throws ModbusIOException { } /** - * Writes the request/response message to the port + * Calculates the estimated serial character throughput based on the current port configuration. * - * @param msg Message to write - * @throws ModbusIOException If the port throws an error + * @return Characters per second. Never less than 10, which serves as a + * defensive fallback for invalid or unexpected port settings. */ - private void writeMessage(ModbusMessage msg) throws ModbusIOException { - open(); - notifyListenersBeforeWrite(msg); - try { - writeMessageOut(msg); - long startTime = System.nanoTime(); - - // Wait here for the message to have been sent - double bytesPerSec = getBytesPerSecond(); - double transmissionTimeNanos = 1_000_000_000.0 * msg.getOutputLength() / bytesPerSec; - waitForTransmission(startTime, transmissionTimeNanos); - } - finally { - notifyListenersAfterWrite(msg); - } - } - - private double getBytesPerSecond() { + private double getCharactersPerSecond() { final double baudRate = commPort.getBaudRate(); final double startBit = 1.0; final double dataBits = commPort.getNumDataBits() == 0 ? 8 : commPort.getNumDataBits(); final double stopBits = commPort.getNumStopBits() == 0 ? 1 : commPort.getNumStopBits(); final double parityBits = commPort.getParity() == SerialPort.NO_PARITY ? 0 : 1; - return Math.max(10.0, baudRate / ( startBit + dataBits + stopBits + parityBits )); + return Math.max(10.0, baudRate / (startBit + dataBits + stopBits + parityBits)); } private void waitForTransmission(long startTime, double transmissionTimeNanos) { - final long sleepMillis = (long) Math.floor(transmissionTimeNanos / NS_IN_A_MS); - final double sleepNanos = transmissionTimeNanos % NS_IN_A_MS; - - if (sleepMillis > 0) { + if (transmissionTimeNanos >= NS_IN_A_MS) { try { - final double fudgedNanoSleep = sleepNanos * NANOS_SLEEP_FUDGE_FACTOR_LONG; - - final long totalSleepMillis = (long) ((sleepMillis * MILLIS_SLEEP_FUDGE_FACTOR) + (fudgedNanoSleep / NS_IN_A_MS)); - final int totalSleepNanos = (int) fudgedNanoSleep % NS_IN_A_MS; + final long adjustedDelay = (long) (transmissionTimeNanos * LONG_DELAY_FUDGE_FACTOR); + final long sleepMillis = adjustedDelay / NS_IN_A_MS; + final int sleepNanos = (int) (adjustedDelay % NS_IN_A_MS); - Thread.sleep(totalSleepMillis, totalSleepNanos); - } catch (InterruptedException e) { + Thread.sleep(sleepMillis, sleepNanos); + } + catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.debug("nothing to do. Sleep interrupted.", e); } - } else if (sleepNanos > 0) { + } + else if (transmissionTimeNanos > 0) { // 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 final int priority = Thread.currentThread().getPriority(); try { Thread.currentThread().setPriority(Thread.MIN_PRIORITY); - long end = startTime + (long) (sleepNanos * NANOS_SLEEP_FUDGE_FACTOR_SHORT); + final long adjustedDelay = (long) (transmissionTimeNanos * SHORT_DELAY_FUDGE_FACTOR); + final long end = startTime + adjustedDelay; while (System.nanoTime() < end) { // noop } - } finally { + } + finally { Thread.currentThread().setPriority(priority); } } } + /** + * Writes the request/response message to the port + * + * @param msg Message to write + * @throws ModbusIOException If the port throws an error + */ + private void writeMessage(ModbusMessage msg) throws ModbusIOException { + open(); + notifyListenersBeforeWrite(msg); + try { + writeMessageOut(msg); + final long startTime = System.nanoTime(); + + // Wait here for the message to have been sent + final double transmissionTimeNanos = 1_000_000_000.0 * msg.getOutputLength() / getCharactersPerSecond(); + waitForTransmission(startTime, transmissionTimeNanos); + } + finally { + notifyListenersAfterWrite(msg); + } + } + @Override public ModbusRequest readRequest(AbstractModbusListener listener) throws ModbusIOException { open(); @@ -227,7 +231,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; @@ -237,7 +240,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; @@ -423,7 +425,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 { @@ -467,7 +468,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 { @@ -484,7 +484,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 { @@ -530,7 +529,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 { @@ -569,7 +567,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 { From e2959209d6cb9e88a32d78b6710c9be40c85d9b3 Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Wed, 12 Aug 2026 09:31:19 +0200 Subject: [PATCH 07/17] improve * move `getBitsPerCharacter` to `AbstractSerialConnection` * fix stopBits value --- .../modbus/io/ModbusSerialTransport.java | 27 ++++-------- .../modbus/net/AbstractSerialConnection.java | 42 ++++++++++++++++++- 2 files changed, 49 insertions(+), 20 deletions(-) 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 8cf122e9..2fa71136 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -58,6 +58,12 @@ public abstract class ModbusSerialTransport extends AbstractModbusTransport { * The number of nanoseconds there is in a millisecond */ private static final int NS_IN_A_MS = 1_000_000; + + /** + * The number of nanoseconds there is in a second + */ + private static final int NS_IN_A_SEC = 1_000_000_000; + 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"; @@ -103,22 +109,6 @@ public void writeRequest(ModbusRequest msg) throws ModbusIOException { writeMessage(msg); } - /** - * Calculates the estimated serial character throughput based on the current port configuration. - * - * @return Characters per second. Never less than 10, which serves as a - * defensive fallback for invalid or unexpected port settings. - */ - private double getCharactersPerSecond() { - final double baudRate = commPort.getBaudRate(); - final double startBit = 1.0; - final double dataBits = commPort.getNumDataBits() == 0 ? 8 : commPort.getNumDataBits(); - final double stopBits = commPort.getNumStopBits() == 0 ? 1 : commPort.getNumStopBits(); - final double parityBits = commPort.getParity() == SerialPort.NO_PARITY ? 0 : 1; - - return Math.max(10.0, baudRate / (startBit + dataBits + stopBits + parityBits)); - } - private void waitForTransmission(long startTime, double transmissionTimeNanos) { if (transmissionTimeNanos >= NS_IN_A_MS) { try { @@ -165,7 +155,8 @@ private void writeMessage(ModbusMessage msg) throws ModbusIOException { final long startTime = System.nanoTime(); // Wait here for the message to have been sent - final double transmissionTimeNanos = 1_000_000_000.0 * msg.getOutputLength() / getCharactersPerSecond(); + final double charactersPerSecond = commPort.getBaudRate() / commPort.getBitsPerCharacter(); + final double transmissionTimeNanos = NS_IN_A_SEC * msg.getOutputLength() / charactersPerSecond; waitForTransmission(startTime, transmissionTimeNanos); } finally { @@ -691,7 +682,7 @@ 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(); + return (long) (chars * NS_IN_A_MS * commPort.getBitsPerCharacter() / commPort.getBaudRate()); } /** 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..ee64f97d 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/net/AbstractSerialConnection.java +++ b/src/main/java/com/ghgande/j2mod/modbus/net/AbstractSerialConnection.java @@ -115,12 +115,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 +201,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() == SerialPort.NO_PARITY ? 0 : 1; + + return startBit + dataBits + stopBits + parityBits; + } + } From e93dcd8c1b74e1709017879242634d26b2e514bc Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Wed, 12 Aug 2026 17:28:59 +0200 Subject: [PATCH 08/17] quickgix --- .../java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2fa71136..0ef57f10 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -156,7 +156,7 @@ private void writeMessage(ModbusMessage msg) throws ModbusIOException { // Wait here for the message to have been sent final double charactersPerSecond = commPort.getBaudRate() / commPort.getBitsPerCharacter(); - final double transmissionTimeNanos = NS_IN_A_SEC * msg.getOutputLength() / charactersPerSecond; + final double transmissionTimeNanos = (msg.getOutputLength() / charactersPerSecond) * NS_IN_A_SEC; waitForTransmission(startTime, transmissionTimeNanos); } finally { From 994c3cef4adb72768de28b8458f6fdb8ad947541 Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Wed, 12 Aug 2026 19:00:17 +0200 Subject: [PATCH 09/17] quickfix --- .../java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0ef57f10..dae002c2 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -62,7 +62,7 @@ public abstract class ModbusSerialTransport extends AbstractModbusTransport { /** * The number of nanoseconds there is in a second */ - private static final int NS_IN_A_SEC = 1_000_000_000; + private static final long NS_IN_A_SEC = 1_000_000_000L; 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"; From 7d6a41b8600452f4ded4b6d37ecb95c667ca392a Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Wed, 12 Aug 2026 20:46:00 +0200 Subject: [PATCH 10/17] catch RuntimeExceptions --- .../ghgande/j2mod/modbus/io/ModbusSerialTransport.java | 8 ++++++++ .../j2mod/modbus/net/AbstractSerialConnection.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) 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 dae002c2..8f15ea2c 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -122,6 +122,10 @@ private void waitForTransmission(long startTime, double transmissionTimeNanos) { Thread.currentThread().interrupt(); logger.debug("nothing to do. Sleep interrupted.", e); } + catch (RuntimeException ex) { + Thread.currentThread().interrupt(); + logger.debug("nothing to do.", ex); + } } else if (transmissionTimeNanos > 0) { // For delays less than a millisecond, we need to chew CPU cycles unfortunately @@ -135,6 +139,10 @@ else if (transmissionTimeNanos > 0) { // noop } } + catch (RuntimeException ex) { + Thread.currentThread().interrupt(); + logger.debug("nothing to do.", ex); + } finally { Thread.currentThread().setPriority(priority); } 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 ee64f97d..68400d32 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/net/AbstractSerialConnection.java +++ b/src/main/java/com/ghgande/j2mod/modbus/net/AbstractSerialConnection.java @@ -212,7 +212,7 @@ public double getBitsPerCharacter() { final int numDataBits = getNumDataBits(); final int dataBits = numDataBits == 0 ? 8 : numDataBits; final double stopBits = getStopBits(); - final double parityBits = getParity() == SerialPort.NO_PARITY ? 0 : 1; + final double parityBits = getParity() == NO_PARITY ? 0 : 1; return startBit + dataBits + stopBits + parityBits; } From c72cfbabbec87a562353ef2513b4bf8839b778fd Mon Sep 17 00:00:00 2001 From: da Kai Date: Wed, 12 Aug 2026 23:03:57 +0200 Subject: [PATCH 11/17] Fix serial timing and constructor chaining MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix getCharIntervalMicro() to correctly return microseconds (was returning nanoseconds due to using NS_IN_A_MS instead of MICROS_IN_A_SEC) - Refactor SerialParameters constructors to use proper delegation (no-arg → full-arg chain) - Add baud rate validation (must be >= 1) to prevent division-by-zero in timing calculations - Rename DEFAULT_RS485_TX_DURING_RX to DEFAULT_RS485_RX_DURING_TX for clarity - Fix spurious Thread.currentThread().interrupt() calls on non-InterruptedException paths - Minor log message improvements for waitForTransmission --- .../modbus/io/ModbusSerialTransport.java | 30 +++--- .../modbus/net/AbstractSerialConnection.java | 6 +- .../j2mod/modbus/util/SerialParameters.java | 91 ++++++++++--------- 3 files changed, 70 insertions(+), 57 deletions(-) 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 8f15ea2c..7481465b 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -55,14 +55,19 @@ 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 = 1_000_000; + private static final double NS_IN_A_MS = 1_000_000.0; /** - * The number of nanoseconds there is in a second + * The number of microseconds in a second. */ - private static final long NS_IN_A_SEC = 1_000_000_000L; + 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"; @@ -113,18 +118,17 @@ private void waitForTransmission(long startTime, double transmissionTimeNanos) { if (transmissionTimeNanos >= NS_IN_A_MS) { try { final long adjustedDelay = (long) (transmissionTimeNanos * LONG_DELAY_FUDGE_FACTOR); - final long sleepMillis = adjustedDelay / NS_IN_A_MS; + final long sleepMillis = (long) (adjustedDelay / NS_IN_A_MS); final int sleepNanos = (int) (adjustedDelay % NS_IN_A_MS); Thread.sleep(sleepMillis, sleepNanos); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - logger.debug("nothing to do. Sleep interrupted.", e); + logger.debug("waitForTransmission interrupted. Ignoring.", e); } catch (RuntimeException ex) { - Thread.currentThread().interrupt(); - logger.debug("nothing to do.", ex); + logger.debug("waitForTransmission failed with exception. Ignoring.", ex); } } else if (transmissionTimeNanos > 0) { @@ -140,8 +144,7 @@ else if (transmissionTimeNanos > 0) { } } catch (RuntimeException ex) { - Thread.currentThread().interrupt(); - logger.debug("nothing to do.", ex); + logger.debug("waitForTransmission failed with exception. Ignoring.", ex); } finally { Thread.currentThread().setPriority(priority); @@ -637,7 +640,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; @@ -661,7 +664,7 @@ 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); } } @@ -690,7 +693,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 * commPort.getBitsPerCharacter() / 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 68400d32..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(); 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..973adc64 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("Invalid baud rate: " + 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; } /** From 576c30527e5058087023e3f2d9bbe4db0f17bd78 Mon Sep 17 00:00:00 2001 From: da Kai Date: Thu, 13 Aug 2026 20:24:33 +0200 Subject: [PATCH 12/17] optimize transmission wait delay and CPU usage - Replace thread priority manipulation with a hybrid wait strategy (sleep -> yield -> busy spin). - Anchor target end time dynamically using System.nanoTime() inside waitForTransmission. - Prevent thread starvation and reduce syscall overhead during sub-millisecond delays. - Improve IllegalArgumentException message in SerialParameters for invalid baud rate. --- .../modbus/io/ModbusSerialTransport.java | 65 +++++++++---------- .../j2mod/modbus/util/SerialParameters.java | 2 +- 2 files changed, 31 insertions(+), 36 deletions(-) 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 7481465b..504a32fb 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; @@ -114,41 +113,38 @@ public void writeRequest(ModbusRequest msg) throws ModbusIOException { writeMessage(msg); } - private void waitForTransmission(long startTime, double transmissionTimeNanos) { - if (transmissionTimeNanos >= NS_IN_A_MS) { - try { - final long adjustedDelay = (long) (transmissionTimeNanos * LONG_DELAY_FUDGE_FACTOR); - final long sleepMillis = (long) (adjustedDelay / NS_IN_A_MS); - final int sleepNanos = (int) (adjustedDelay % NS_IN_A_MS); + private void waitForTransmission(double transmissionTimeNanos) { + if (transmissionTimeNanos <= 0) { + return; + } - Thread.sleep(sleepMillis, sleepNanos); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.debug("waitForTransmission interrupted. Ignoring.", e); - } - catch (RuntimeException ex) { - logger.debug("waitForTransmission failed with exception. Ignoring.", ex); + 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 >= 2_000_000L) { + long sleepMillis = (long) ((remainingNanos - 1_000_000L) / NS_IN_A_MS); + Thread.sleep(sleepMillis); } - } - else if (transmissionTimeNanos > 0) { - // 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 - final int priority = Thread.currentThread().getPriority(); - try { - Thread.currentThread().setPriority(Thread.MIN_PRIORITY); - final long adjustedDelay = (long) (transmissionTimeNanos * SHORT_DELAY_FUDGE_FACTOR); - final long end = startTime + adjustedDelay; - while (System.nanoTime() < end) { - // noop + do { + remainingNanos = targetEndNanos - System.nanoTime(); + if (remainingNanos > 40_000L) { + // Between 40µs and 2ms: Yield CPU + Thread.yield(); + } else { + // <= 40µs: Pure busy wait } - } - catch (RuntimeException ex) { - logger.debug("waitForTransmission failed with exception. Ignoring.", ex); - } - finally { - Thread.currentThread().setPriority(priority); - } + } while (remainingNanos > 0); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.debug("waitForTransmission interrupted. Ignoring.", e); + } + catch (RuntimeException ex) { + logger.debug("waitForTransmission failed with exception. Ignoring.", ex); } } @@ -163,12 +159,11 @@ private void writeMessage(ModbusMessage msg) throws ModbusIOException { notifyListenersBeforeWrite(msg); try { writeMessageOut(msg); - final long startTime = System.nanoTime(); // Wait here for the message to have been sent final double charactersPerSecond = commPort.getBaudRate() / commPort.getBitsPerCharacter(); final double transmissionTimeNanos = (msg.getOutputLength() / charactersPerSecond) * NS_IN_A_SEC; - waitForTransmission(startTime, transmissionTimeNanos); + waitForTransmission(transmissionTimeNanos); } finally { notifyListenersAfterWrite(msg); 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 973adc64..f67dece8 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/util/SerialParameters.java +++ b/src/main/java/com/ghgande/j2mod/modbus/util/SerialParameters.java @@ -225,7 +225,7 @@ public void setPortName(String name) { */ public void setBaudRate(int rate) { if (rate < 1) { - throw new IllegalArgumentException("Invalid baud rate: " + rate); + throw new IllegalArgumentException("Baud rate must be greater than 0, but was: " + rate); } baudRate = rate; } From 2c5ab6fd225d0b77f30b36ef6ba83e6883bce61e Mon Sep 17 00:00:00 2001 From: da Kai Date: Thu, 13 Aug 2026 21:52:35 +0200 Subject: [PATCH 13/17] add tests --- .../modbus/net/SerialConnectionTest.java | 85 +++++++++++++++++++ .../modbus/util/SerialParametersTest.java | 10 +++ 2 files changed, 95 insertions(+) create mode 100644 src/test/java/com/ghgande/j2mod/modbus/net/SerialConnectionTest.java 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 From acb5624b9b8e232b8e991faec754df421737f4c7 Mon Sep 17 00:00:00 2001 From: da Kai Date: Thu, 13 Aug 2026 23:17:18 +0200 Subject: [PATCH 14/17] Fix RTU inter-char timeout and delay handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align serial timing with MODBUS over Serial Line v1.02 by using a 750µs inter-character timeout for baud rates above 19200 and keeping 1750µs for inter-frame delay. Renames `getMaxCharDelay()` to `getMaxCharTimeout()` and updates RTU receive logic to use the correct timeout value. Also simplifies the nanos wait loop to sleep until close to target, then busy-wait for precise timing. --- .../j2mod/modbus/io/ModbusRTUTransport.java | 4 +- .../modbus/io/ModbusSerialTransport.java | 38 ++++++++++--------- 2 files changed, 23 insertions(+), 19 deletions(-) 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 504a32fb..1ec0e764 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -125,19 +125,13 @@ private void waitForTransmission(double transmissionTimeNanos) { try { long remainingNanos = targetEndNanos - System.nanoTime(); - if (remainingNanos >= 2_000_000L) { - long sleepMillis = (long) ((remainingNanos - 1_000_000L) / NS_IN_A_MS); + if (remainingNanos >= 1_750_000L) { + long sleepMillis = (long) ((remainingNanos - 750_000L) / NS_IN_A_MS); Thread.sleep(sleepMillis); } - do { - remainingNanos = targetEndNanos - System.nanoTime(); - if (remainingNanos > 40_000L) { - // Between 40µs and 2ms: Yield CPU - Thread.yield(); - } else { - // <= 40µs: Pure busy wait - } - } while (remainingNanos > 0); + while (System.nanoTime() < targetEndNanos) { + // Pure busy wait + }; } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -649,9 +643,14 @@ void waitBetweenFrames(int transDelayMS, long lastTransactionTimestamp) { } /** - * In microseconds + * Calculates the inter-frame delay according to the + * MODBUS over Serial Line Specification V1.02. + *

    + *
  • baud rates ≤ 19200: 3.5 Character time
  • + *
  • baud rates > 19200: 1750 microseconds
  • + *
* - * @return Delay between frames + * @return the inter-frame delay in microseconds */ int getInterFrameDelay() { if (commPort.getBaudRate() > 19200) { @@ -664,13 +663,18 @@ int getInterFrameDelay() { } /** - * The maximum delay between characters in microseconds + * Calculates the inter-character time-out according to the + * MODBUS over Serial Line Specification V1.02. + *
    + *
  • baud rates ≤ 19200: 1.5 Character time
  • + *
  • baud rates > 19200: 750 microseconds
  • + *
* - * @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); From d1a7baa86edd6e409d4e8e7ba20d995984394d15 Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Fri, 14 Aug 2026 07:55:52 +0200 Subject: [PATCH 15/17] Improve serial transport delay yielding Reduce CPU-heavy waiting in `ModbusSerialTransport` by yielding the thread before the final busy-spin for longer transmission delays. This keeps the precise timing behavior while giving the scheduler a chance to run other work. --- .../ghgande/j2mod/modbus/io/ModbusSerialTransport.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 1ec0e764..1e0689d9 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -129,9 +129,16 @@ private void waitForTransmission(double transmissionTimeNanos) { long sleepMillis = (long) ((remainingNanos - 750_000L) / 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(); From d6c02f9be8926f1fc0b384c8d4153c7dac10c756 Mon Sep 17 00:00:00 2001 From: da Kai <99220919+da-Kai@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:29:17 +0200 Subject: [PATCH 16/17] replace magic numbers with sleep constants --- .../j2mod/modbus/io/ModbusSerialTransport.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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 1e0689d9..7c1ccc6a 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java +++ b/src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java @@ -71,6 +71,19 @@ public abstract class ModbusSerialTransport extends AbstractModbusTransport { 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. */ @@ -125,8 +138,8 @@ private void waitForTransmission(double transmissionTimeNanos) { try { long remainingNanos = targetEndNanos - System.nanoTime(); - if (remainingNanos >= 1_750_000L) { - long sleepMillis = (long) ((remainingNanos - 750_000L) / NS_IN_A_MS); + 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) { From a55ab83652fad58bcdb21d49c6c887ae83342898 Mon Sep 17 00:00:00 2001 From: Kai Jeschek Date: Wed, 19 Aug 2026 10:25:08 +0200 Subject: [PATCH 17/17] cleanup --- .../com/ghgande/j2mod/modbus/slave/ModbusSlaveFactory.java | 1 + .../java/com/ghgande/j2mod/modbus/util/SerialParameters.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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 f67dece8..d96ea123 100644 --- a/src/main/java/com/ghgande/j2mod/modbus/util/SerialParameters.java +++ b/src/main/java/com/ghgande/j2mod/modbus/util/SerialParameters.java @@ -219,7 +219,7 @@ public void setPortName(String name) { } /** - * Sets the baud rate. Has to be >=1. + * Sets the baud rate. Has to be ≥1. * * @param rate the new baud rate. */ @@ -242,7 +242,7 @@ public void setBaudRate(String rate) { /** * Return the baud rate as int. *

- * Is guaranteed to return a value >=1 + * Is guaranteed to return a value ≥1 * * @return the baud rate as int. */