Skip to content

Commit 1fc16eb

Browse files
authored
Add rtu modbus test (#8)
* introduce modbus serial test using mocked port/devices * added LINT,ULINT,LREAL
1 parent e1fd046 commit 1fc16eb

2 files changed

Lines changed: 867 additions & 16 deletions

File tree

agent/src/main/java/org/openremote/agent/protocol/modbus/ModbusSerialProtocol.java

Lines changed: 163 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,62 @@ public class ModbusSerialProtocol extends AbstractProtocol<ModbusSerialAgent, Mo
4949
protected final Map<String, List<BatchReadRequest>> cachedBatches = new ConcurrentHashMap<>(); // Cached batch requests per group
5050
protected final Map<String, ScheduledFuture<?>> batchPollingTasks = new ConcurrentHashMap<>();
5151
private final Object modbusLock = new Object();
52-
private SerialPort serialPort;
52+
private SerialPortWrapper serialPort;
5353
private String connectionString;
5454
private Set<RegisterRange> illegalRegisters;
55-
55+
56+
// Serial port wrapper interface for testing
57+
public interface SerialPortWrapper {
58+
boolean openPort();
59+
boolean closePort();
60+
boolean isOpen();
61+
int writeBytes(byte[] buffer, long bytesToWrite);
62+
int readBytes(byte[] buffer, long bytesToRead, long offset);
63+
int bytesAvailable();
64+
}
65+
66+
// Real SerialPort implementation
67+
private static class RealSerialPortWrapper implements SerialPortWrapper {
68+
private final SerialPort port;
69+
70+
RealSerialPortWrapper(SerialPort port) {
71+
this.port = port;
72+
}
73+
74+
@Override
75+
public boolean openPort() {
76+
return port.openPort();
77+
}
78+
79+
@Override
80+
public boolean closePort() {
81+
return port.closePort();
82+
}
83+
84+
@Override
85+
public boolean isOpen() {
86+
return port.isOpen();
87+
}
88+
89+
@Override
90+
public int writeBytes(byte[] buffer, long bytesToWrite) {
91+
return port.writeBytes(buffer, (int) bytesToWrite);
92+
}
93+
94+
@Override
95+
public int readBytes(byte[] buffer, long bytesToRead, long offset) {
96+
return port.readBytes(buffer, (int) bytesToRead, (int) offset);
97+
}
98+
99+
@Override
100+
public int bytesAvailable() {
101+
return port.bytesAvailable();
102+
}
103+
}
104+
105+
// For testing: inject a mock serial port wrapper
106+
public static SerialPortWrapper mockSerialPortForTesting = null;
107+
56108
public ModbusSerialProtocol(ModbusSerialAgent agent) {
57109
super(agent);
58110
}
@@ -86,12 +138,19 @@ protected void doStart(Container container) throws Exception {
86138

87139
connectionString = "modbus-rtu://" + portName + "?baud=" + baudRate + "&data=" + dataBits + "&stop=" + stopBits + "&parity=" + parity;
88140

89-
serialPort = SerialPort.getCommPort(portName);
90-
serialPort.setBaudRate(baudRate);
91-
serialPort.setNumDataBits(dataBits);
92-
serialPort.setNumStopBits(stopBits);
93-
serialPort.setParity(mapParityToSerialPort(agent.getParityValue()));
94-
serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 50, 0);
141+
// Use mock serial port for testing if available
142+
if (mockSerialPortForTesting != null) {
143+
serialPort = mockSerialPortForTesting;
144+
LOG.info("Using mock serial port for testing");
145+
} else {
146+
SerialPort sp = SerialPort.getCommPort(portName);
147+
sp.setBaudRate(baudRate);
148+
sp.setNumDataBits(dataBits);
149+
sp.setNumStopBits(stopBits);
150+
sp.setParity(mapParityToSerialPort(agent.getParityValue()));
151+
sp.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 50, 0);
152+
serialPort = new RealSerialPortWrapper(sp);
153+
}
95154

96155
if (serialPort.openPort()) {
97156
setConnectionStatus(ConnectionStatus.CONNECTED);
@@ -357,6 +416,47 @@ private Object parseModbusResponse(byte[] response, byte functionCode, ModbusAge
357416
buffer.order(ByteOrder.BIG_ENDIAN);
358417
return buffer.getInt();
359418
}
419+
} else if (byteCount == 8) {
420+
// Four registers - could be 64-bit integer or double precision float
421+
byte[] dataBytes = new byte[8];
422+
System.arraycopy(response, 3, dataBytes, 0, 8);
423+
424+
if (dataType == ModbusAgentLink.ModbusDataType.LREAL) {
425+
ByteBuffer buffer = ByteBuffer.wrap(dataBytes);
426+
buffer.order(ByteOrder.BIG_ENDIAN);
427+
double value = buffer.getDouble();
428+
429+
// Filter out NaN and Infinity values to prevent database issues
430+
if (Double.isNaN(value) || Double.isInfinite(value)) {
431+
LOG.warning("Modbus response contains invalid double value (NaN or Infinity), ignoring update");
432+
return null;
433+
}
434+
435+
return value;
436+
} else if (dataType == ModbusAgentLink.ModbusDataType.LINT) {
437+
// 64-bit signed integer
438+
ByteBuffer buffer = ByteBuffer.wrap(dataBytes);
439+
buffer.order(ByteOrder.BIG_ENDIAN);
440+
return buffer.getLong();
441+
} else if (dataType == ModbusAgentLink.ModbusDataType.ULINT) {
442+
// 64-bit unsigned integer - use BigInteger
443+
ByteBuffer buffer = ByteBuffer.wrap(dataBytes);
444+
buffer.order(ByteOrder.BIG_ENDIAN);
445+
long signedValue = buffer.getLong();
446+
447+
// Convert to unsigned BigInteger
448+
if (signedValue >= 0) {
449+
return java.math.BigInteger.valueOf(signedValue);
450+
} else {
451+
// Handle negative as unsigned
452+
return java.math.BigInteger.valueOf(signedValue).add(java.math.BigInteger.ONE.shiftLeft(64));
453+
}
454+
} else {
455+
// Default: treat as 64-bit signed integer
456+
ByteBuffer buffer = ByteBuffer.wrap(dataBytes);
457+
buffer.order(ByteOrder.BIG_ENDIAN);
458+
return buffer.getLong();
459+
}
360460
}
361461
}
362462

@@ -437,7 +537,7 @@ private boolean writeSingleHoldingRegister(int unitId, int address, Object value
437537
private int readWithTimeout(byte[] buffer, long timeoutMs) throws InterruptedException {
438538
long startTime = System.currentTimeMillis();
439539
int totalBytesRead = 0;
440-
540+
441541
while (totalBytesRead < buffer.length && (System.currentTimeMillis() - startTime) < timeoutMs) {
442542
int available = serialPort.bytesAvailable();
443543
if (available > 0) {
@@ -446,7 +546,7 @@ private int readWithTimeout(byte[] buffer, long timeoutMs) throws InterruptedExc
446546
}
447547
Thread.sleep(5);
448548
}
449-
549+
450550
return totalBytesRead;
451551
}
452552

@@ -720,6 +820,46 @@ private Object extractValueFromBatchResponse(byte[] response, int registerOffset
720820
buffer.order(ByteOrder.BIG_ENDIAN);
721821
return buffer.getInt();
722822
}
823+
} else if (registerCount == 4) {
824+
// Four registers - could be 64-bit integer or double precision float
825+
byte[] dataBytes = new byte[8];
826+
System.arraycopy(response, byteOffset, dataBytes, 0, 8);
827+
828+
if (dataType == ModbusAgentLink.ModbusDataType.LREAL) {
829+
ByteBuffer buffer = ByteBuffer.wrap(dataBytes);
830+
buffer.order(ByteOrder.BIG_ENDIAN);
831+
double value = buffer.getDouble();
832+
833+
if (Double.isNaN(value) || Double.isInfinite(value)) {
834+
LOG.warning("Batch response contains invalid double value (NaN or Infinity), ignoring");
835+
return null;
836+
}
837+
838+
return value;
839+
} else if (dataType == ModbusAgentLink.ModbusDataType.LINT) {
840+
// 64-bit signed integer
841+
ByteBuffer buffer = ByteBuffer.wrap(dataBytes);
842+
buffer.order(ByteOrder.BIG_ENDIAN);
843+
return buffer.getLong();
844+
} else if (dataType == ModbusAgentLink.ModbusDataType.ULINT) {
845+
// 64-bit unsigned integer - use BigInteger
846+
ByteBuffer buffer = ByteBuffer.wrap(dataBytes);
847+
buffer.order(ByteOrder.BIG_ENDIAN);
848+
long signedValue = buffer.getLong();
849+
850+
// Convert to unsigned BigInteger
851+
if (signedValue >= 0) {
852+
return java.math.BigInteger.valueOf(signedValue);
853+
} else {
854+
// Handle negative as unsigned
855+
return java.math.BigInteger.valueOf(signedValue).add(java.math.BigInteger.ONE.shiftLeft(64));
856+
}
857+
} else {
858+
// Default: treat as 64-bit signed integer
859+
ByteBuffer buffer = ByteBuffer.wrap(dataBytes);
860+
buffer.order(ByteOrder.BIG_ENDIAN);
861+
return buffer.getLong();
862+
}
723863
}
724864
}
725865
}
@@ -923,12 +1063,19 @@ private void resetAgent() {
9231063
int dataBits = agent.getDataBits();
9241064
int stopBits = agent.getStopBits();
9251065

926-
serialPort = SerialPort.getCommPort(portName);
927-
serialPort.setBaudRate(baudRate);
928-
serialPort.setNumDataBits(dataBits);
929-
serialPort.setNumStopBits(stopBits);
930-
serialPort.setParity(mapParityToSerialPort(agent.getParityValue()));
931-
serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 50, 0);
1066+
// Use mock serial port for testing if available
1067+
if (mockSerialPortForTesting != null) {
1068+
serialPort = mockSerialPortForTesting;
1069+
LOG.info("Using mock serial port for testing (reset)");
1070+
} else {
1071+
SerialPort sp = SerialPort.getCommPort(portName);
1072+
sp.setBaudRate(baudRate);
1073+
sp.setNumDataBits(dataBits);
1074+
sp.setNumStopBits(stopBits);
1075+
sp.setParity(mapParityToSerialPort(agent.getParityValue()));
1076+
sp.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 50, 0);
1077+
serialPort = new RealSerialPortWrapper(sp);
1078+
}
9321079

9331080
if (!serialPort.openPort()) {
9341081
setConnectionStatus(ConnectionStatus.ERROR);

0 commit comments

Comments
 (0)