Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
140 changes: 93 additions & 47 deletions src/main/java/com/ghgande/j2mod/modbus/io/ModbusSerialTransport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<AbstractSerialTransportListener> listeners = Collections.synchronizedSet(new HashSet<AbstractSerialTransportListener>());
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das verursacht 100% CPU Auslastung auf einem Kern, solange es läuft. Sollte man, wie früher, davor und danach die Thread Priorität auf LOW stellen?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wenn überhaupt, würde ich eher Thread.yield() verwenden. Aber damit nimmt man sich die Genauigkeit.

Auf der anderen Seite hat die aktuelle implementierung die factoren x1,3 bzw. x1,7 drin. Also ist das Timing sowieso nicht exakt.

Leider ist im Code nirgends dokumentiert, wofür diese Fudge-Faktoren gedacht sind oder wie sie ermittelt wurden.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aber wo ruft man Thread.yield() auf? Thread.yield() in jedem while Durchlauf auszuführen ist auch nicht gut. Macht es vllt. Sinn j2mod auf Java 21 upzudaten und die neue Thread.sleep() Implementierung mit dem nanoseconds Übergabeparameter zu nutzen?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread.sleep(millis, nanos) gibt es auch in Java 8 schon. Ein sleep() schickt den Thread aber immer an den OS-Scheduler und ist deshalb oft um mehrere Millisekunden ungenau, egal was man an Nanos übergibt. Java nutzt die Nanos eher zum Runden.

Für echte nano- oder mikrosekunden-Genauigkeit kommen wir um ein busy-wait nicht herum.

Zum entlasten im busy-wait sehe ich zwei möglichkeiten:

  1. Thread.yield(): Da gibt es aber keine Garantie, was das OS daraus macht. Es könnte Compute freigeben, oder sich wie ein reiner Busy-Wait verhalten. Ist wieder völlig OS abhängig.

  2. Thread.onSpinWait(): Dafür müssten wir auf Java 9 Upgraden. Damit kann man innerhalb des busy-wait der CPU signalisieren, dass sie compute freigeben kann.

Du kannst hier gerne was ergänzen, falls du mehr weißt, aber das ist, was ich mir bisher schon zusammen gereimt habe.

// 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
*
Expand All @@ -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);
Expand Down Expand Up @@ -200,7 +242,6 @@ public void setTimeout(int time) {
*
* @param listener Listener that received this request
* @return a <code>ModbusRequest</code> value
*
* @throws ModbusIOException if an error occurs
*/
protected abstract ModbusRequest readRequestIn(AbstractModbusListener listener) throws ModbusIOException;
Expand All @@ -210,7 +251,6 @@ public void setTimeout(int time) {
* responding to a master writeRequest request.
*
* @return a <code>ModbusResponse</code> value
*
* @throws ModbusIOException if an error occurs
*/
protected abstract ModbusResponse readResponseIn() throws ModbusIOException;
Expand Down Expand Up @@ -328,7 +368,7 @@ public void notifyListenersDisconnected() {
}
}
}

/**
* <code>setCommPort</code> sets the comm port member and prepares the input
* and output streams to be used for reading from and writing to.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand All @@ -628,28 +663,38 @@ void waitBetweenFrames(int transDelayMS, long lastTransactionTimestamp) {
}

/**
* In microseconds
* Calculates the inter-frame delay according to the
* MODBUS over Serial Line Specification V1.02.
* <ul>
* <li> baud rates &le; 19200: 3.5 Character time </li>
* <li> baud rates &gt; 19200: 1750 microseconds </li>
* </ul>
*
* @return Delay between frames
* @return the inter-frame delay in microseconds
*/
int getInterFrameDelay() {
if (commPort.getBaudRate() > 19200) {
return 1750;
}
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.
* <ul>
* <li> baud rates &le; 19200: 1.5 Character time </li>
* <li> baud rates &gt; 19200: 750 microseconds </li>
* </ul>
*
* @return microseconds
* @return the inter-character time-out in microseconds
*/
long getMaxCharDelay() {
long getMaxCharTimeout() {
Comment thread
Howaner marked this conversation as resolved.
if (commPort.getBaudRate() > 19200) {
return 1750;
return 750;
Comment thread
Howaner marked this conversation as resolved.
}
else {
return getCharIntervalMicro(Modbus.INTER_CHARACTER_GAP);
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,11 @@ public abstract class AbstractSerialConnection {
public abstract void close();

/**
* Returns current baud rate
* Returns current baud rate.
* <p>
* 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();

Expand All @@ -115,12 +117,34 @@ public abstract class AbstractSerialConnection {
public abstract int getNumDataBits();

/**
* Returns current stop bits
* Returns current stop bits configuration constant.
* <p>
* 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.
* <p>
* Use {@link #getNumStopBits()} to get the stop bits configuration constant.
*
* @return Stop-bit length in bit times.
*/
public float getStopBits() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das Naming passt nicht. Es werden nicht die StopBits zurückgegeben, sondern die Bit Times.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich hätte es so gelassen. In der Modbus Serial Specification wird auch nicht zwischen Stop Bits und Stop Bit Times unterschieden.
Ich habe den Zusatz "in bit times." ergänzt, um den 1,5 wert zu erklären. Aber es ist heute sowieso eher unüblich den zu verwenden.

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
*
Expand Down Expand Up @@ -179,4 +203,20 @@ public abstract class AbstractSerialConnection {
*/
public abstract Set<String> 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() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das Naming ist nicht gut. Bei getBitsPerCharacter wird wahrscheinlich an die Data Bits gedacht. Besser wäre getBitsToTransferPerCharacter()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Es sind ja die Bits, die ein Character für die Übertragung braucht. Da es ja um Datenübertragung geht, würde ich Bit Times und Bits in diesem Zusammenhang gleichsetzen.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Es geht nicht um den Kommentar, sondern um den Funktionsnamen. Weil es eben um die Übertragung geht, würde ich die Funktion getBitsToTransferPerCharacter() nennen. Kommentare ließt niemand.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich verstehe, das Clean-Code möglichst präzise namen bevorzugt. Ich würde aber Argumentieren, dass es im Kontext von AbstractSerialConnection keinen Unterschied zwischen getBitsToTransferPerCharacter und getBitsPerCharacter gibt und ich dann eher zum einfachern Namen greifen würde.

Gleichzeitig wäre es mir aber auch nicht besonders wichtig. @michaelgrill als unparteiischer Dritter?

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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Loading