├── CLAUDE.md ├── .git-blame-ignore-revs ├── modbus ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── digitalpetri │ │ │ └── modbus │ │ │ ├── pdu │ │ │ ├── ModbusRequestPdu.java │ │ │ ├── ModbusResponsePdu.java │ │ │ ├── ModbusPdu.java │ │ │ ├── WriteSingleRegisterRequest.java │ │ │ ├── WriteSingleRegisterResponse.java │ │ │ ├── WriteMultipleRegistersResponse.java │ │ │ ├── ReadCoilsRequest.java │ │ │ ├── ReadDiscreteInputsRequest.java │ │ │ ├── WriteMultipleCoilsResponse.java │ │ │ ├── WriteSingleCoilResponse.java │ │ │ ├── MaskWriteRegisterRequest.java │ │ │ ├── ReadInputRegistersRequest.java │ │ │ ├── MaskWriteRegisterResponse.java │ │ │ ├── ReadHoldingRegistersRequest.java │ │ │ ├── WriteSingleCoilRequest.java │ │ │ ├── ReadDiscreteInputsResponse.java │ │ │ ├── ReadInputRegistersResponse.java │ │ │ ├── ReadHoldingRegistersResponse.java │ │ │ ├── ReadWriteMultipleRegistersResponse.java │ │ │ ├── ReadCoilsResponse.java │ │ │ ├── WriteMultipleCoilsRequest.java │ │ │ ├── WriteMultipleRegistersRequest.java │ │ │ └── ReadWriteMultipleRegistersRequest.java │ │ │ ├── client │ │ │ ├── ModbusTcpClientTransport.java │ │ │ ├── ModbusRtuClientTransport.java │ │ │ ├── ModbusClientTransport.java │ │ │ └── ModbusClientConfig.java │ │ │ ├── internal │ │ │ └── util │ │ │ │ ├── package-info.java │ │ │ │ ├── Hex.java │ │ │ │ ├── BufferPool.java │ │ │ │ └── ExecutionQueue.java │ │ │ ├── ExceptionResponse.java │ │ │ ├── server │ │ │ ├── ModbusRtuServerTransport.java │ │ │ ├── ModbusTcpServerTransport.java │ │ │ ├── authz │ │ │ │ ├── AuthzContext.java │ │ │ │ ├── ReadWriteAuthzHandler.java │ │ │ │ └── AuthzHandler.java │ │ │ ├── ModbusServer.java │ │ │ ├── ModbusServerTransport.java │ │ │ ├── ModbusRequestContext.java │ │ │ ├── ModbusServerConfig.java │ │ │ └── ReadOnlyModbusServices.java │ │ │ ├── exceptions │ │ │ ├── UnknownUnitIdException.java │ │ │ ├── ModbusException.java │ │ │ ├── ModbusConnectException.java │ │ │ ├── ModbusTimeoutException.java │ │ │ ├── ModbusExecutionException.java │ │ │ ├── ModbusCrcException.java │ │ │ └── ModbusResponseException.java │ │ │ ├── ModbusTcpFrame.java │ │ │ ├── ModbusRtuFrame.java │ │ │ ├── TimeoutScheduler.java │ │ │ ├── MbapHeader.java │ │ │ ├── Crc16.java │ │ │ ├── FunctionCode.java │ │ │ ├── ExceptionCode.java │ │ │ ├── ModbusRtuRequestFrameParser.java │ │ │ ├── ModbusRtuResponseFrameParser.java │ │ │ └── Modbus.java │ └── test │ │ └── java │ │ └── com │ │ └── digitalpetri │ │ └── modbus │ │ ├── MbapHeaderTest.java │ │ ├── client │ │ ├── DefaultTransactionSequenceTest.java │ │ ├── ModbusRtuClientTest.java │ │ └── ModbusTcpClientTest.java │ │ ├── pdu │ │ ├── ReadCoilsResponseTest.java │ │ ├── ReadInputRegistersResponseTest.java │ │ ├── ReadHoldingRegistersResponseTest.java │ │ ├── ReadDiscreteInputsResponseTest.java │ │ ├── ReadCoilsRequestTest.java │ │ ├── ReadWriteMultipleRegistersResponseTest.java │ │ ├── WriteSingleCoilRequestTest.java │ │ ├── WriteSingleCoilResponseTest.java │ │ ├── WriteSingleRegisterRequestTest.java │ │ ├── ReadInputRegistersRequestTest.java │ │ ├── WriteSingleRegisterResponseTest.java │ │ ├── ReadDiscreteInputsRequestTest.java │ │ ├── ReadHoldingRegistersRequestTest.java │ │ ├── WriteMultipleCoilsResponseTest.java │ │ ├── WriteMultipleRegistersResponseTest.java │ │ ├── MaskWriteRegisterRequestTest.java │ │ ├── MaskWriteRegisterResponseTest.java │ │ ├── WriteMultipleCoilsRequestTest.java │ │ ├── WriteMultipleRegistersRequestTest.java │ │ └── ReadWriteMultipleRegistersRequestTest.java │ │ ├── Util.java │ │ ├── Crc16Test.java │ │ ├── ModbusRtuRequestFrameParserTest.java │ │ └── ModbusRtuResponseFrameParserTest.java └── pom.xml ├── .gitignore ├── .github └── workflows │ ├── maven.yml │ ├── maven-deploy-snapshot.yml │ ├── copilot-setup-steps.yml │ └── maven-release.yml ├── modbus-tcp ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── digitalpetri │ │ │ └── modbus │ │ │ └── tcp │ │ │ ├── client │ │ │ └── NettyTimeoutScheduler.java │ │ │ ├── ModbusTcpCodec.java │ │ │ ├── Netty.java │ │ │ ├── server │ │ │ └── NettyRequestContext.java │ │ │ └── security │ │ │ └── SecurityUtil.java │ └── test │ │ └── java │ │ └── com │ │ └── digitalpetri │ │ └── modbus │ │ └── tcp │ │ └── ModbusTcpCodecTest.java └── pom.xml ├── modbus-serial └── pom.xml ├── README.md └── modbus-tests ├── src └── test │ └── java │ └── com │ └── digitalpetri │ └── modbus │ └── test │ ├── ModbusRtuTcpClientServerIT.java │ ├── ModbusRtuClientServerIT.java │ ├── ModbusTcpTlsClientServerIT.java │ ├── ModbusTcpClientServerIT.java │ └── ClientServerIT.java └── pom.xml /CLAUDE.md: -------------------------------------------------------------------------------- 1 | @AGENTS.md 2 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # google-java-format 2 | f7e52171266d287916a3d853b30ec3d7757b0566 3 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ModbusRequestPdu.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | /** Super-interface for Modbus request PDUs. */ 4 | public interface ModbusRequestPdu extends ModbusPdu {} 5 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ModbusResponsePdu.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | /** Super-interface for Modbus response PDUs. */ 4 | public interface ModbusResponsePdu extends ModbusPdu {} 5 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/client/ModbusTcpClientTransport.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.client; 2 | 3 | import com.digitalpetri.modbus.ModbusTcpFrame; 4 | 5 | public interface ModbusTcpClientTransport extends ModbusClientTransport {} 6 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/internal/util/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal classes used by the library. 3 | * 4 | *

These classes are not part of the public API and should not be used by clients. 5 | */ 6 | package com.digitalpetri.modbus.internal.util; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java ### 2 | *.class 3 | *.jar 4 | *.war 5 | *.ear 6 | hs_err_pid* 7 | 8 | ### Maven ### 9 | target/ 10 | pom.xml.tag 11 | pom.xml.releaseBackup 12 | pom.xml.versionsBackup 13 | pom.xml.next 14 | release.properties 15 | 16 | ### Intellij ### 17 | .idea/ 18 | *.ipr 19 | *.iws 20 | *.iml 21 | 22 | ### External src and other files ### 23 | external/ 24 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ModbusPdu.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | /** Super-interface for objects that can be encoded as a Modbus PDU. */ 4 | public interface ModbusPdu { 5 | 6 | /** 7 | * Get the function code identifying this PDU. 8 | * 9 | * @return the function code identifying this PDU. 10 | */ 11 | int getFunctionCode(); 12 | } 13 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/ExceptionResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | /** 4 | * Modbus Exception Response. 5 | * 6 | * @param functionCode the {@link FunctionCode} that elicited this response. 7 | * @param exceptionCode the {@link ExceptionCode} indicated by the outstation. 8 | */ 9 | public record ExceptionResponse(FunctionCode functionCode, ExceptionCode exceptionCode) {} 10 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/client/ModbusRtuClientTransport.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.client; 2 | 3 | import com.digitalpetri.modbus.ModbusRtuFrame; 4 | 5 | public interface ModbusRtuClientTransport extends ModbusClientTransport { 6 | 7 | /** 8 | * Reset the frame parser. 9 | * 10 | *

This method should be called after a timeout or CRC error to reset the parser state. 11 | */ 12 | void resetFrameParser(); 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | pull_request: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set up JDK 17 14 | uses: actions/setup-java@v4 15 | with: 16 | java-version: '17' 17 | distribution: 'temurin' 18 | cache: maven 19 | 20 | - name: Build with Maven 21 | run: mvn -B package 22 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/ModbusRtuServerTransport.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server; 2 | 3 | import com.digitalpetri.modbus.ModbusRtuFrame; 4 | import com.digitalpetri.modbus.server.ModbusRequestContext.ModbusRtuRequestContext; 5 | 6 | /** 7 | * Modbus/RTU server transport; a {@link ModbusServerTransport} that sends and receives {@link 8 | * ModbusRtuFrame}s. 9 | */ 10 | public interface ModbusRtuServerTransport 11 | extends ModbusServerTransport {} 12 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/ModbusTcpServerTransport.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server; 2 | 3 | import com.digitalpetri.modbus.ModbusTcpFrame; 4 | import com.digitalpetri.modbus.server.ModbusRequestContext.ModbusTcpRequestContext; 5 | 6 | /** 7 | * Modbus/TCP server transport; a {@link ModbusServerTransport} that sends and receives {@link 8 | * ModbusTcpFrame}s. 9 | */ 10 | public interface ModbusTcpServerTransport 11 | extends ModbusServerTransport {} 12 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/exceptions/UnknownUnitIdException.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.exceptions; 2 | 3 | import java.io.Serial; 4 | 5 | public class UnknownUnitIdException extends ModbusException { 6 | 7 | @Serial private static final long serialVersionUID = 58792353863854093L; 8 | 9 | public UnknownUnitIdException(int unitId) { 10 | super("unknown unit id: " + unitId); 11 | } 12 | 13 | public UnknownUnitIdException(int unitId, Throwable cause) { 14 | super("unknown unit id: " + unitId, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/exceptions/ModbusException.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.exceptions; 2 | 3 | import java.io.Serial; 4 | 5 | public class ModbusException extends Exception { 6 | 7 | @Serial private static final long serialVersionUID = 5355236996267676988L; 8 | 9 | public ModbusException(String message) { 10 | super(message); 11 | } 12 | 13 | public ModbusException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public ModbusException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/exceptions/ModbusConnectException.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.exceptions; 2 | 3 | import java.io.Serial; 4 | 5 | public class ModbusConnectException extends ModbusException { 6 | 7 | @Serial private static final long serialVersionUID = -5350159787088895451L; 8 | 9 | public ModbusConnectException(String message) { 10 | super(message); 11 | } 12 | 13 | public ModbusConnectException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public ModbusConnectException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/exceptions/ModbusTimeoutException.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.exceptions; 2 | 3 | import java.io.Serial; 4 | 5 | public class ModbusTimeoutException extends ModbusException { 6 | 7 | @Serial private static final long serialVersionUID = -8643809775979891078L; 8 | 9 | public ModbusTimeoutException(String message) { 10 | super(message); 11 | } 12 | 13 | public ModbusTimeoutException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public ModbusTimeoutException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/exceptions/ModbusExecutionException.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.exceptions; 2 | 3 | import java.io.Serial; 4 | 5 | public class ModbusExecutionException extends ModbusException { 6 | 7 | @Serial private static final long serialVersionUID = 8407528717209895345L; 8 | 9 | public ModbusExecutionException(String message) { 10 | super(message); 11 | } 12 | 13 | public ModbusExecutionException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public ModbusExecutionException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/authz/AuthzContext.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server.authz; 2 | 3 | import java.security.cert.X509Certificate; 4 | import java.util.Optional; 5 | 6 | public interface AuthzContext { 7 | 8 | /** 9 | * Get the role of the client attempting the operation, if available. 10 | * 11 | * @return the role of the client attempting the operation, if available. 12 | */ 13 | Optional clientRole(); 14 | 15 | /** 16 | * Get the client certificate chain. 17 | * 18 | * @return the client certificate chain. 19 | */ 20 | X509Certificate[] clientCertificateChain(); 21 | } 22 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/ModbusServer.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server; 2 | 3 | public interface ModbusServer { 4 | 5 | /** 6 | * Start the server. 7 | * 8 | * @throws Exception if the server could not be started. 9 | */ 10 | void start() throws Exception; 11 | 12 | /** 13 | * Stop the server. 14 | * 15 | * @throws Exception if the server could not be stopped. 16 | */ 17 | void stop() throws Exception; 18 | 19 | /** 20 | * Set the {@link ModbusServices} that will be used to handle requests. 21 | * 22 | * @param services the {@link ModbusServices} to use. 23 | */ 24 | void setModbusServices(ModbusServices services); 25 | } 26 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/MbapHeaderTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class MbapHeaderTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int i = 0; i < 65536; i++) { 13 | ByteBuffer buffer = ByteBuffer.allocate(7); 14 | 15 | var header = new MbapHeader(i, i, i, i % 256); 16 | MbapHeader.Serializer.encode(header, buffer); 17 | 18 | buffer.flip(); 19 | MbapHeader decoded = MbapHeader.Serializer.decode(buffer); 20 | 21 | assertEquals(header, decoded); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/exceptions/ModbusCrcException.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.exceptions; 2 | 3 | import com.digitalpetri.modbus.ModbusRtuFrame; 4 | import java.io.Serial; 5 | 6 | public class ModbusCrcException extends ModbusException { 7 | 8 | @Serial private static final long serialVersionUID = -5350159787088895451L; 9 | 10 | private final ModbusRtuFrame frame; 11 | 12 | public ModbusCrcException(ModbusRtuFrame frame) { 13 | super("CRC mismatch"); 14 | 15 | this.frame = frame; 16 | } 17 | 18 | /** 19 | * Get the frame that caused the exception. 20 | * 21 | * @return the frame that caused the exception. 22 | */ 23 | public ModbusRtuFrame getFrame() { 24 | return frame; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/client/DefaultTransactionSequenceTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.client; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.digitalpetri.modbus.client.ModbusTcpClient.DefaultTransactionSequence; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class DefaultTransactionSequenceTest { 9 | 10 | @Test 11 | void rollover() { 12 | DefaultTransactionSequence sequence = new DefaultTransactionSequence(); 13 | 14 | // Assert that transactions are generated in the range [0, 65535] 15 | // and that they roll over back to 0. 16 | for (int i = 0; i < 2; i++) { 17 | for (int id = 0; id < 65536; id++) { 18 | assertEquals(id, sequence.next()); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadCoilsResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.util.Random; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class ReadCoilsResponseTest { 10 | 11 | @Test 12 | void serializer() { 13 | var buffer = ByteBuffer.allocate(256); 14 | 15 | byte[] bs = new byte[10]; 16 | new Random().nextBytes(bs); 17 | 18 | var response = new ReadCoilsResponse(bs); 19 | ReadCoilsResponse.Serializer.encode(response, buffer); 20 | 21 | buffer.flip(); 22 | 23 | ReadCoilsResponse decoded = ReadCoilsResponse.Serializer.decode(buffer); 24 | 25 | assertEquals(response, decoded); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/maven-deploy-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Maven Deploy Snapshot 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set up Maven Central Repository 14 | uses: actions/setup-java@v4 15 | with: 16 | java-version: '17' 17 | distribution: 'temurin' 18 | server-id: central 19 | server-username: MAVEN_USERNAME 20 | server-password: MAVEN_PASSWORD 21 | 22 | - name: Publish package 23 | run: mvn -B clean deploy -P deploy-snapshot 24 | env: 25 | MAVEN_USERNAME: ${{ secrets.CENTRAL_SONATYPE_TOKEN_USERNAME }} 26 | MAVEN_PASSWORD: ${{ secrets.CENTRAL_SONATYPE_TOKEN_PASSWORD }} 27 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/Util.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import java.util.Arrays; 4 | import java.util.stream.IntStream; 5 | import java.util.stream.Stream; 6 | 7 | public class Util { 8 | 9 | static Stream partitions(byte[] source, int partitionSize) { 10 | int size = source.length; 11 | 12 | if (size == 0) { 13 | return Stream.empty(); 14 | } 15 | 16 | int fullChunks = (size - 1) / partitionSize; 17 | 18 | return IntStream.range(0, fullChunks + 1) 19 | .mapToObj( 20 | n -> { 21 | int fromIndex = n * partitionSize; 22 | int toIndex = n == fullChunks ? size : (n + 1) * partitionSize; 23 | 24 | return Arrays.copyOfRange(source, fromIndex, toIndex); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/Crc16Test.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class Crc16Test { 9 | 10 | @Test 11 | void crc16() { 12 | // https://crccalc.com/?crc=123456789&method=crc16&datatype=hex&outtype=0 13 | Crc16 crc = new Crc16(); 14 | crc.update(ByteBuffer.wrap(new byte[] {0x12, 0x34, 0x56, 0x78, 0x09})); 15 | int value = crc.getValue(); 16 | 17 | assertEquals(0x2590, value); 18 | } 19 | 20 | @Test 21 | void reset() { 22 | Crc16 crc = new Crc16(); 23 | crc.update(ByteBuffer.wrap(new byte[] {0x12, 0x34, 0x56, 0x78, 0x09})); 24 | crc.reset(); 25 | 26 | assertEquals(0xFFFF, crc.getValue()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadInputRegistersResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class ReadInputRegistersResponseTest { 9 | 10 | @Test 11 | public void serializer() { 12 | byte[] registers = new byte[] {0x01, 0x02, 0x03, 0x04}; 13 | var response = new ReadInputRegistersResponse(registers); 14 | 15 | ByteBuffer buffer = ByteBuffer.allocate(256); 16 | ReadInputRegistersResponse.Serializer.encode(response, buffer); 17 | 18 | buffer.flip(); 19 | 20 | ReadInputRegistersResponse decoded = ReadInputRegistersResponse.Serializer.decode(buffer); 21 | 22 | assertEquals(response, decoded); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/ModbusTcpFrame.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import com.digitalpetri.modbus.internal.util.Hex; 4 | import java.nio.ByteBuffer; 5 | import java.util.StringJoiner; 6 | 7 | /** 8 | * Modbus/TCP frame data, an {@link MbapHeader} and encoded PDU. 9 | * 10 | * @param header the {@link MbapHeader} for this frame. 11 | * @param pdu the encoded Modbus PDU data. 12 | */ 13 | public record ModbusTcpFrame(MbapHeader header, ByteBuffer pdu) { 14 | 15 | @Override 16 | public String toString() { 17 | // note: overridden to give preferred representation of `pdu` bytes 18 | return new StringJoiner(", ", ModbusTcpFrame.class.getSimpleName() + "[", "]") 19 | .add("header=" + header) 20 | .add("pdu=" + Hex.format(pdu)) 21 | .toString(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadHoldingRegistersResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class ReadHoldingRegistersResponseTest { 9 | 10 | @Test 11 | void serializer() { 12 | byte[] registers = new byte[] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; 13 | var response = new ReadHoldingRegistersResponse(registers); 14 | 15 | ByteBuffer buffer = ByteBuffer.allocate(256); 16 | ReadHoldingRegistersResponse.Serializer.encode(response, buffer); 17 | 18 | buffer.flip(); 19 | 20 | ReadHoldingRegistersResponse decoded = ReadHoldingRegistersResponse.Serializer.decode(buffer); 21 | 22 | assertEquals(response, decoded); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadDiscreteInputsResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.util.Random; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class ReadDiscreteInputsResponseTest { 10 | 11 | @Test 12 | public void serializer() { 13 | ByteBuffer buffer = ByteBuffer.allocate(256); 14 | 15 | byte[] bs = new byte[10]; 16 | new Random().nextBytes(bs); 17 | 18 | var response = new ReadDiscreteInputsResponse(bs); 19 | ReadDiscreteInputsResponse.Serializer.encode(response, buffer); 20 | 21 | buffer.flip(); 22 | 23 | ReadDiscreteInputsResponse decoded = ReadDiscreteInputsResponse.Serializer.decode(buffer); 24 | 25 | assertEquals(response, decoded); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadCoilsRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class ReadCoilsRequestTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (short quantity = 1; quantity <= 2000; quantity++) { 14 | ByteBuffer buffer = ByteBuffer.allocate(256); 15 | 16 | var request = new ReadCoilsRequest(address, quantity); 17 | ReadCoilsRequest.Serializer.encode(request, buffer); 18 | 19 | buffer.flip(); 20 | 21 | ReadCoilsRequest decoded = ReadCoilsRequest.Serializer.decode(buffer); 22 | 23 | assertEquals(request, decoded); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadWriteMultipleRegistersResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class ReadWriteMultipleRegistersResponseTest { 9 | 10 | @Test 11 | void serialize() { 12 | byte[] registers = new byte[] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; 13 | var response = new ReadWriteMultipleRegistersResponse(registers); 14 | 15 | ByteBuffer buffer = ByteBuffer.allocate(256); 16 | ReadWriteMultipleRegistersResponse.Serializer.encode(response, buffer); 17 | 18 | buffer.flip(); 19 | 20 | ReadWriteMultipleRegistersResponse decoded = 21 | ReadWriteMultipleRegistersResponse.Serializer.decode(buffer); 22 | 23 | assertEquals(response, decoded); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/WriteSingleCoilRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class WriteSingleCoilRequestTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (boolean value : new boolean[] {true, false}) { 14 | ByteBuffer buffer = ByteBuffer.allocate(256); 15 | 16 | var request = new WriteSingleCoilRequest(address, value); 17 | WriteSingleCoilRequest.Serializer.encode(request, buffer); 18 | 19 | buffer.flip(); 20 | 21 | WriteSingleCoilRequest decoded = WriteSingleCoilRequest.Serializer.decode(buffer); 22 | 23 | assertEquals(request, decoded); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/WriteSingleCoilResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class WriteSingleCoilResponseTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (boolean value : new boolean[] {true, false}) { 14 | var response = new WriteSingleCoilResponse(address, value); 15 | ByteBuffer buffer = ByteBuffer.allocate(256); 16 | 17 | WriteSingleCoilResponse.Serializer.encode(response, buffer); 18 | 19 | buffer.flip(); 20 | 21 | WriteSingleCoilResponse decoded = WriteSingleCoilResponse.Serializer.decode(buffer); 22 | 23 | assertEquals(response, decoded); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/WriteSingleRegisterRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class WriteSingleRegisterRequestTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (int value : new int[] {0, 1, 0xFFFF}) { 14 | ByteBuffer buffer = ByteBuffer.allocate(256); 15 | 16 | var request = new WriteSingleRegisterRequest(address, value); 17 | WriteSingleRegisterRequest.Serializer.encode(request, buffer); 18 | 19 | buffer.flip(); 20 | 21 | WriteSingleRegisterRequest decoded = WriteSingleRegisterRequest.Serializer.decode(buffer); 22 | 23 | assertEquals(request, decoded); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadInputRegistersRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class ReadInputRegistersRequestTest { 9 | 10 | @Test 11 | public void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (short quantity = 1; quantity <= 125; quantity++) { 14 | ByteBuffer buffer = ByteBuffer.allocate(256); 15 | 16 | var request = new ReadInputRegistersRequest(address, quantity); 17 | ReadInputRegistersRequest.Serializer.encode(request, buffer); 18 | 19 | buffer.flip(); 20 | 21 | ReadInputRegistersRequest decoded = ReadInputRegistersRequest.Serializer.decode(buffer); 22 | 23 | assertEquals(request, decoded); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/WriteSingleRegisterResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class WriteSingleRegisterResponseTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (int value : new int[] {0, 1, 0xFFFF}) { 14 | ByteBuffer buffer = ByteBuffer.allocate(256); 15 | 16 | var response = new WriteSingleRegisterResponse(address, value); 17 | 18 | WriteSingleRegisterResponse.Serializer.encode(response, buffer); 19 | 20 | buffer.flip(); 21 | 22 | WriteSingleRegisterResponse decoded = WriteSingleRegisterResponse.Serializer.decode(buffer); 23 | 24 | assertEquals(response, decoded); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadDiscreteInputsRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class ReadDiscreteInputsRequestTest { 9 | 10 | @Test 11 | public void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (short quantity = 1; quantity <= 2000; quantity++) { 14 | ByteBuffer buffer = ByteBuffer.allocate(256); 15 | 16 | var request = new ReadDiscreteInputsRequest(address, quantity); 17 | ReadDiscreteInputsRequest.Serializer.encode(request, buffer); 18 | 19 | buffer.flip(); 20 | 21 | ReadDiscreteInputsRequest decoded = ReadDiscreteInputsRequest.Serializer.decode(buffer); 22 | 23 | assertEquals(request, decoded); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadHoldingRegistersRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class ReadHoldingRegistersRequestTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (short quantity = 1; quantity <= 125; quantity++) { 14 | ByteBuffer buffer = ByteBuffer.allocate(256); 15 | 16 | var request = new ReadHoldingRegistersRequest(address, quantity); 17 | ReadHoldingRegistersRequest.Serializer.encode(request, buffer); 18 | 19 | buffer.flip(); 20 | 21 | ReadHoldingRegistersRequest decoded = ReadHoldingRegistersRequest.Serializer.decode(buffer); 22 | 23 | assertEquals(request, decoded); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/WriteMultipleCoilsResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class WriteMultipleCoilsResponseTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (int quantity = 1; quantity < 0x07B0; quantity++) { 14 | ByteBuffer buffer = ByteBuffer.allocate(256); 15 | 16 | var response = new WriteMultipleCoilsResponse(address, quantity); 17 | WriteMultipleCoilsResponse.Serializer.encode(response, buffer); 18 | 19 | buffer.flip(); 20 | 21 | WriteMultipleCoilsResponse decoded = WriteMultipleCoilsResponse.Serializer.decode(buffer); 22 | 23 | assertEquals(response, decoded); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/ModbusRtuFrame.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import com.digitalpetri.modbus.internal.util.Hex; 4 | import java.nio.ByteBuffer; 5 | import java.util.StringJoiner; 6 | 7 | /** 8 | * Modbus/RTU frame data, a unit id and encoded PDU. 9 | * 10 | * @param unitId the identifier of a remote slave connected on a physical or logical other bus. 11 | * @param pdu the encoded Modbus PDU data. 12 | * @param crc the CRC bytes. 13 | */ 14 | public record ModbusRtuFrame(int unitId, ByteBuffer pdu, ByteBuffer crc) { 15 | 16 | @Override 17 | public String toString() { 18 | // note: overridden to give preferred representation of `pdu` and `crc` bytes 19 | return new StringJoiner(", ", ModbusRtuFrame.class.getSimpleName() + "[", "]") 20 | .add("unitId=" + unitId) 21 | .add("pdu=" + Hex.format(pdu)) 22 | .add("crc=" + Hex.format(crc)) 23 | .toString(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/WriteMultipleRegistersResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class WriteMultipleRegistersResponseTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address++) { 13 | for (int quantity = 1; quantity < 0x007B; quantity++) { 14 | ByteBuffer buffer = ByteBuffer.allocate(256); 15 | 16 | var response = new WriteMultipleRegistersResponse(address, quantity); 17 | WriteMultipleRegistersResponse.Serializer.encode(response, buffer); 18 | 19 | buffer.flip(); 20 | 21 | WriteMultipleRegistersResponse decoded = 22 | WriteMultipleRegistersResponse.Serializer.decode(buffer); 23 | 24 | assertEquals(response, decoded); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/copilot-setup-steps.yml: -------------------------------------------------------------------------------- 1 | name: "Copilot Setup Steps" 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - .github/workflows/copilot-setup-steps.yml 8 | pull_request: 9 | paths: 10 | - .github/workflows/copilot-setup-steps.yml 11 | 12 | jobs: 13 | # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. 14 | copilot-setup-steps: 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | contents: read 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v5 23 | 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v5 26 | with: 27 | java-version: "17" 28 | distribution: "temurin" 29 | cache: "maven" 30 | 31 | - name: Install 32 | run: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V 33 | 34 | - name: Download external sources 35 | run: mvn generate-resources -Pdownload-external-src 36 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/MaskWriteRegisterRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class MaskWriteRegisterRequestTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address += 256) { 13 | for (int andMask = 0; andMask <= 0xFFFF; andMask += 256) { 14 | for (int orMask = 0; orMask <= 0xFFFF; orMask += 256) { 15 | ByteBuffer buffer = ByteBuffer.allocate(256); 16 | 17 | var request = new MaskWriteRegisterRequest(address, andMask, orMask); 18 | MaskWriteRegisterRequest.Serializer.encode(request, buffer); 19 | 20 | buffer.flip(); 21 | 22 | MaskWriteRegisterRequest decoded = MaskWriteRegisterRequest.Serializer.decode(buffer); 23 | 24 | assertEquals(request, decoded); 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/MaskWriteRegisterResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class MaskWriteRegisterResponseTest { 9 | 10 | @Test 11 | void serializer() { 12 | for (int address = 0; address < 0xFFFF; address += 256) { 13 | for (int andMask = 0; andMask <= 0xFFFF; andMask += 256) { 14 | for (int orMask = 0; orMask <= 0xFFFF; orMask += 256) { 15 | ByteBuffer buffer = ByteBuffer.allocate(256); 16 | 17 | var response = new MaskWriteRegisterResponse(address, andMask, orMask); 18 | MaskWriteRegisterResponse.Serializer.encode(response, buffer); 19 | 20 | buffer.flip(); 21 | 22 | MaskWriteRegisterResponse decoded = MaskWriteRegisterResponse.Serializer.decode(buffer); 23 | 24 | assertEquals(response, decoded); 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/WriteMultipleCoilsRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class WriteMultipleCoilsRequestTest { 10 | 11 | @Test 12 | void serializer() { 13 | for (int address = 0; address < 0xFFFF; address++) { 14 | for (int quantity = 0; quantity < 0x07B0; quantity++) { 15 | ByteBuffer buffer = ByteBuffer.allocate(256); 16 | 17 | byte[] values = new byte[(quantity + 7) / 8]; 18 | Arrays.fill(values, (byte) 0xFF); 19 | 20 | var request = new WriteMultipleCoilsRequest(address, quantity, values); 21 | WriteMultipleCoilsRequest.Serializer.encode(request, buffer); 22 | 23 | buffer.flip(); 24 | 25 | WriteMultipleCoilsRequest decoded = WriteMultipleCoilsRequest.Serializer.decode(buffer); 26 | 27 | assertEquals(request, decoded); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/internal/util/Hex.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.internal.util; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.HexFormat; 5 | 6 | public class Hex { 7 | 8 | private Hex() {} 9 | 10 | /** 11 | * Format a {@link ByteBuffer} as a hex string. 12 | * 13 | *

Only the bytes between {@link ByteBuffer#position()} and {@link ByteBuffer#limit()} are 14 | * considered. 15 | * 16 | * @param buffer the buffer to format. 17 | * @return the formatted hex string. 18 | */ 19 | public static String format(ByteBuffer buffer) { 20 | StringBuilder sb = new StringBuilder(); 21 | for (int i = buffer.position(); i < buffer.limit(); i++) { 22 | sb.append(String.format("%02x", buffer.get(i))); 23 | } 24 | return sb.toString(); 25 | } 26 | 27 | /** 28 | * Format a byte array as a hex string. 29 | * 30 | * @param bytes the bytes to format. 31 | * @return the formatted hex string. 32 | */ 33 | public static String format(byte[] bytes) { 34 | return HexFormat.of().formatHex(bytes); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/WriteMultipleRegistersRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class WriteMultipleRegistersRequestTest { 10 | 11 | @Test 12 | void serializer() { 13 | for (int address = 0; address < 0xFFFF; address++) { 14 | for (int quantity = 1; quantity < 0x007B; quantity++) { 15 | ByteBuffer buffer = ByteBuffer.allocate(256); 16 | 17 | byte[] values = new byte[quantity * 2]; 18 | Arrays.fill(values, (byte) 0xFF); 19 | 20 | var request = new WriteMultipleRegistersRequest(address, quantity, values); 21 | 22 | WriteMultipleRegistersRequest.Serializer.encode(request, buffer); 23 | 24 | buffer.flip(); 25 | 26 | WriteMultipleRegistersRequest decoded = 27 | WriteMultipleRegistersRequest.Serializer.decode(buffer); 28 | 29 | assertEquals(request, decoded); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/maven-release.yml: -------------------------------------------------------------------------------- 1 | name: Maven Release 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set up Java for publishing to Maven Central 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '17' 21 | distribution: 'temurin' 22 | server-id: central 23 | server-username: MAVEN_USERNAME 24 | server-password: MAVEN_PASSWORD 25 | 26 | - name: Install GPG secret key 27 | run: | 28 | cat <(echo -e "${{ secrets.OSSRH_GPG_SECRET_KEY }}") | gpg --batch --import 29 | gpg --list-secret-keys --keyid-format LONG 30 | 31 | - name: Publish to Maven Central 32 | run: mvn -B clean deploy -P release 33 | env: 34 | MAVEN_USERNAME: ${{ secrets.CENTRAL_SONATYPE_TOKEN_USERNAME }} 35 | MAVEN_PASSWORD: ${{ secrets.CENTRAL_SONATYPE_TOKEN_PASSWORD }} 36 | MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} 37 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/pdu/ReadWriteMultipleRegistersRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.util.Random; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class ReadWriteMultipleRegistersRequestTest { 10 | 11 | @Test 12 | void serializer() { 13 | var random = new Random(); 14 | int address = random.nextInt(0xFFFF + 1); 15 | int quantity = random.nextInt(125) + 1; 16 | int writeAddress = random.nextInt(0xFFFF + 1); 17 | int writeQuantity = random.nextInt(125) + 1; 18 | byte[] writeValues = new byte[writeQuantity * 2]; 19 | random.nextBytes(writeValues); 20 | 21 | ByteBuffer buffer = ByteBuffer.allocate(256); 22 | 23 | var request = 24 | new ReadWriteMultipleRegistersRequest( 25 | address, quantity, writeAddress, writeQuantity, writeValues); 26 | 27 | ReadWriteMultipleRegistersRequest.Serializer.encode(request, buffer); 28 | 29 | buffer.flip(); 30 | 31 | ReadWriteMultipleRegistersRequest decoded = 32 | ReadWriteMultipleRegistersRequest.Serializer.decode(buffer); 33 | 34 | assertEquals(request, decoded); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/client/ModbusClientTransport.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.client; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | import java.util.function.Consumer; 5 | 6 | public interface ModbusClientTransport { 7 | 8 | /** 9 | * Connect this transport. 10 | * 11 | * @return a {@link CompletionStage} that completes when the transport has been connected. 12 | */ 13 | CompletionStage connect(); 14 | 15 | /** 16 | * Disconnect this transport. 17 | * 18 | * @return a {@link CompletionStage} that completes when the transport has been disconnected. 19 | */ 20 | CompletionStage disconnect(); 21 | 22 | /** 23 | * Check if the transport is connected. 24 | * 25 | * @return {@code true} if the transport is connected. 26 | */ 27 | boolean isConnected(); 28 | 29 | /** 30 | * Send a request frame to the transport. 31 | * 32 | * @param frame the request frame to send. 33 | * @return a {@link CompletionStage} that completes when the frame has been sent. 34 | */ 35 | CompletionStage send(T frame); 36 | 37 | /** 38 | * Configure a callback to receive response frames from the transport. 39 | * 40 | * @param frameReceiver the callback to response receive frames. 41 | */ 42 | void receive(Consumer frameReceiver); 43 | } 44 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/ModbusServerTransport.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | 5 | public interface ModbusServerTransport { 6 | 7 | /** 8 | * Bind the transport to its configured local address. 9 | * 10 | * @return a {@link CompletionStage} that completes when the transport is bound. 11 | */ 12 | CompletionStage bind(); 13 | 14 | /** 15 | * Unbind the transport from its configured local address. 16 | * 17 | * @return a {@link CompletionStage} that completes when the transport is unbound. 18 | */ 19 | CompletionStage unbind(); 20 | 21 | /** 22 | * Configure a callback to receive request frames from the transport. 23 | * 24 | * @param frameReceiver the callback to receive request frames. 25 | */ 26 | void receive(FrameReceiver frameReceiver); 27 | 28 | interface FrameReceiver { 29 | 30 | /** 31 | * Receive a request frame from the transport and respond to it. 32 | * 33 | * @param frame the request frame. 34 | * @return a corresponding response frame. 35 | * @throws Exception if there is an unrecoverable error and the channel should be closed. 36 | */ 37 | T receive(C context, T frame) throws Exception; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /modbus-tcp/src/main/java/com/digitalpetri/modbus/tcp/client/NettyTimeoutScheduler.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.tcp.client; 2 | 3 | import com.digitalpetri.modbus.TimeoutScheduler; 4 | import io.netty.util.HashedWheelTimer; 5 | import io.netty.util.Timeout; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.concurrent.atomic.AtomicReference; 8 | 9 | public class NettyTimeoutScheduler implements TimeoutScheduler { 10 | 11 | private final HashedWheelTimer wheelTimer; 12 | 13 | public NettyTimeoutScheduler(HashedWheelTimer wheelTimer) { 14 | this.wheelTimer = wheelTimer; 15 | } 16 | 17 | @Override 18 | public TimeoutHandle newTimeout(Task task, long delay, TimeUnit unit) { 19 | final var ref = new AtomicReference(); 20 | 21 | var handle = 22 | new TimeoutHandle() { 23 | @Override 24 | public void cancel() { 25 | synchronized (ref) { 26 | ref.get().cancel(); 27 | } 28 | } 29 | 30 | @Override 31 | public boolean isCancelled() { 32 | synchronized (ref) { 33 | return ref.get().isCancelled(); 34 | } 35 | } 36 | }; 37 | 38 | synchronized (ref) { 39 | ref.set(wheelTimer.newTimeout(timeout -> task.run(handle), delay, unit)); 40 | } 41 | 42 | return handle; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /modbus-tcp/src/test/java/com/digitalpetri/modbus/tcp/ModbusTcpCodecTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.tcp; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.digitalpetri.modbus.MbapHeader; 6 | import com.digitalpetri.modbus.ModbusTcpFrame; 7 | import io.netty.buffer.ByteBuf; 8 | import io.netty.buffer.ByteBufUtil; 9 | import io.netty.buffer.Unpooled; 10 | import io.netty.channel.embedded.EmbeddedChannel; 11 | import java.nio.ByteBuffer; 12 | import org.junit.jupiter.api.Test; 13 | 14 | class ModbusTcpCodecTest { 15 | 16 | @Test 17 | void encodeDecodeFrame() { 18 | var channel = new EmbeddedChannel(new ModbusTcpCodec()); 19 | 20 | var frame = 21 | new ModbusTcpFrame( 22 | new MbapHeader(0, 0, 5, 0), ByteBuffer.wrap(new byte[] {0x01, 0x02, 0x03, 0x04})); 23 | 24 | channel.writeOutbound(frame); 25 | ByteBuf encoded = channel.readOutbound(); 26 | System.out.println(ByteBufUtil.hexDump(encoded)); 27 | 28 | channel.writeInbound(encoded); 29 | ModbusTcpFrame decoded = channel.readInbound(); 30 | 31 | frame.pdu().flip(); 32 | 33 | System.out.println(frame); 34 | System.out.println(decoded); 35 | 36 | assertEquals(frame, decoded); 37 | } 38 | 39 | @Test 40 | void emptyPdu() { 41 | var rx = Unpooled.copiedBuffer(ByteBufUtil.decodeHexDump("5FFD0000000101")); 42 | var channel = new EmbeddedChannel(new ModbusTcpCodec()); 43 | 44 | channel.writeInbound(rx); 45 | ModbusTcpFrame frame = channel.readInbound(); 46 | 47 | System.out.println(frame); 48 | assertEquals(0, frame.pdu().remaining()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modbus-tcp/src/main/java/com/digitalpetri/modbus/tcp/ModbusTcpCodec.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.tcp; 2 | 3 | import com.digitalpetri.modbus.MbapHeader; 4 | import com.digitalpetri.modbus.ModbusTcpFrame; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.handler.codec.ByteToMessageCodec; 8 | import java.nio.ByteBuffer; 9 | import java.util.List; 10 | 11 | public class ModbusTcpCodec extends ByteToMessageCodec { 12 | 13 | public static final int MBAP_TOTAL_LENGTH = 7; 14 | public static final int MBAP_LENGTH_FIELD_OFFSET = 4; 15 | 16 | @Override 17 | protected void encode(ChannelHandlerContext ctx, ModbusTcpFrame msg, ByteBuf out) { 18 | var buffer = ByteBuffer.allocate(MBAP_TOTAL_LENGTH + msg.pdu().limit() - msg.pdu().position()); 19 | MbapHeader.Serializer.encode(msg.header(), buffer); 20 | buffer.put(msg.pdu()); 21 | 22 | buffer.flip(); 23 | out.writeBytes(buffer); 24 | } 25 | 26 | @Override 27 | protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { 28 | if (in.readableBytes() >= MBAP_TOTAL_LENGTH) { 29 | int frameLength = in.getUnsignedShort(in.readerIndex() + MBAP_LENGTH_FIELD_OFFSET) + 6; 30 | 31 | if (in.readableBytes() >= frameLength) { 32 | ByteBuffer buffer = ByteBuffer.allocate(frameLength); 33 | in.readBytes(buffer); 34 | buffer.flip(); 35 | 36 | MbapHeader header = MbapHeader.Serializer.decode(buffer); 37 | ByteBuffer pdu = buffer.slice(); 38 | 39 | out.add(new ModbusTcpFrame(header, pdu)); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/ModbusRequestContext.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server; 2 | 3 | import com.digitalpetri.modbus.server.ModbusRequestContext.ModbusRtuRequestContext; 4 | import com.digitalpetri.modbus.server.ModbusRequestContext.ModbusTcpRequestContext; 5 | import com.digitalpetri.modbus.server.authz.AuthzContext; 6 | import java.net.SocketAddress; 7 | 8 | /** 9 | * A transport-agnostic super-interface that transport implementations can subclass to smuggle 10 | * transport-specific context information to the application layer. 11 | */ 12 | public sealed interface ModbusRequestContext 13 | permits ModbusRtuRequestContext, ModbusTcpRequestContext { 14 | 15 | non-sealed interface ModbusTcpRequestContext extends ModbusRequestContext { 16 | 17 | /** 18 | * Get the local address that received the request. 19 | * 20 | * @return the local address that received the request. 21 | */ 22 | SocketAddress localAddress(); 23 | 24 | /** 25 | * Get the remote address of the client that sent the request. 26 | * 27 | * @return the remote address of the client that sent the request. 28 | */ 29 | SocketAddress remoteAddress(); 30 | } 31 | 32 | interface ModbusTcpTlsRequestContext extends ModbusTcpRequestContext, AuthzContext {} 33 | 34 | non-sealed interface ModbusRtuRequestContext extends ModbusRequestContext {} 35 | 36 | interface ModbusRtuTcpRequestContext extends ModbusRtuRequestContext, ModbusTcpRequestContext {} 37 | 38 | interface ModbusRtuTlsRequestContext 39 | extends ModbusRtuRequestContext, ModbusTcpTlsRequestContext {} 40 | } 41 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/exceptions/ModbusResponseException.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.exceptions; 2 | 3 | import com.digitalpetri.modbus.ExceptionCode; 4 | import com.digitalpetri.modbus.FunctionCode; 5 | import java.io.Serial; 6 | 7 | public class ModbusResponseException extends ModbusException { 8 | 9 | @Serial private static final long serialVersionUID = -4058366691447836220L; 10 | 11 | private final int functionCode; 12 | private final int exceptionCode; 13 | 14 | public ModbusResponseException(int functionCode, int exceptionCode) { 15 | super(createMessage(functionCode, exceptionCode)); 16 | 17 | this.functionCode = functionCode; 18 | this.exceptionCode = exceptionCode; 19 | } 20 | 21 | public ModbusResponseException(FunctionCode functionCode, ExceptionCode exceptionCode) { 22 | this(functionCode.getCode(), exceptionCode.getCode()); 23 | } 24 | 25 | /** 26 | * @return the function code that generated the exception response. 27 | */ 28 | public int getFunctionCode() { 29 | return functionCode; 30 | } 31 | 32 | /** 33 | * @return the exception code indicated in the exception response. 34 | */ 35 | public int getExceptionCode() { 36 | return exceptionCode; 37 | } 38 | 39 | private static String createMessage(int functionCode, int exceptionCode) { 40 | String fcs = FunctionCode.from(functionCode).map(Enum::toString).orElse("UNKNOWN"); 41 | String ecs = ExceptionCode.from(exceptionCode).map(Enum::toString).orElse("UNKNOWN"); 42 | 43 | return "0x%02X [%s] generated exception response 0x%02X [%s]" 44 | .formatted(functionCode, fcs, exceptionCode, ecs); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/TimeoutScheduler.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import com.digitalpetri.modbus.internal.util.ExecutionQueue; 4 | import java.util.concurrent.Executor; 5 | import java.util.concurrent.ScheduledExecutorService; 6 | import java.util.concurrent.ScheduledFuture; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.concurrent.atomic.AtomicReference; 9 | 10 | public interface TimeoutScheduler { 11 | 12 | TimeoutHandle newTimeout(Task task, long delay, TimeUnit unit); 13 | 14 | interface Task { 15 | 16 | void run(TimeoutHandle handle); 17 | } 18 | 19 | interface TimeoutHandle { 20 | 21 | void cancel(); 22 | 23 | boolean isCancelled(); 24 | } 25 | 26 | static TimeoutScheduler create(Executor executor, ScheduledExecutorService scheduledExecutor) { 27 | return (task, delay, unit) -> { 28 | final var ref = new AtomicReference>(); 29 | final ExecutionQueue queue = new ExecutionQueue(executor); 30 | 31 | var handle = 32 | new TimeoutHandle() { 33 | @Override 34 | public void cancel() { 35 | synchronized (ref) { 36 | ref.get().cancel(false); 37 | } 38 | } 39 | 40 | @Override 41 | public boolean isCancelled() { 42 | synchronized (ref) { 43 | return ref.get().isCancelled(); 44 | } 45 | } 46 | }; 47 | 48 | synchronized (ref) { 49 | ScheduledFuture future = 50 | scheduledExecutor.schedule(() -> queue.submit(() -> task.run(handle)), delay, unit); 51 | ref.set(future); 52 | } 53 | 54 | return handle; 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/MbapHeader.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | /** 6 | * Modbus Application Protocol header for frames that encapsulates Modbus request and response PDUs 7 | * on TCP/IP. 8 | * 9 | * @param transactionId transaction identifier. 2 bytes, identifies a request/response transaction. 10 | * @param protocolId protocol identifier. 2 bytes, always 0 for Modbus protocol. 11 | * @param length number of bytes that follow, including 1 for the unit id. 2 bytes. 12 | * @param unitId identifier of a remote slave connected on a physical or logical other bus. 1 byte. 13 | */ 14 | public record MbapHeader(int transactionId, int protocolId, int length, int unitId) { 15 | 16 | /** Utility functions for encoding and decoding {@link MbapHeader}. */ 17 | public static class Serializer { 18 | 19 | private Serializer() {} 20 | 21 | /** 22 | * Encode a {@link MbapHeader} into a {@link ByteBuffer}. 23 | * 24 | * @param header the header to encode. 25 | * @param buffer the buffer to encode into. 26 | */ 27 | public static void encode(MbapHeader header, ByteBuffer buffer) { 28 | buffer.putShort((short) header.transactionId); 29 | buffer.putShort((short) header.protocolId); 30 | buffer.putShort((short) header.length); 31 | buffer.put((byte) header.unitId); 32 | } 33 | 34 | /** 35 | * Decode a {@link MbapHeader} from a {@link ByteBuffer}. 36 | * 37 | * @param buffer the buffer to decode from. 38 | * @return the decoded header. 39 | */ 40 | public static MbapHeader decode(ByteBuffer buffer) { 41 | int transactionId = buffer.getShort() & 0xFFFF; 42 | int protocolId = buffer.getShort() & 0xFFFF; 43 | int length = buffer.getShort() & 0xFFFF; 44 | int unitId = buffer.get() & 0xFF; 45 | 46 | return new MbapHeader(transactionId, protocolId, length, unitId); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/WriteSingleRegisterRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#WRITE_SINGLE_REGISTER} request PDU. 8 | * 9 | * @param address the address of the register to write. 2 bytes, range [0x0000, 0xFFFF]. 10 | * @param value the value to write. 2 bytes, range [0x0000, 0xFFFF]. 11 | */ 12 | public record WriteSingleRegisterRequest(int address, int value) implements ModbusRequestPdu { 13 | 14 | @Override 15 | public int getFunctionCode() { 16 | return FunctionCode.WRITE_SINGLE_REGISTER.getCode(); 17 | } 18 | 19 | /** Utility functions for encoding and decoding {@link WriteSingleRegisterRequest}. */ 20 | public static final class Serializer { 21 | 22 | private Serializer() {} 23 | 24 | /** 25 | * Encode a {@link WriteSingleRegisterRequest} into a {@link ByteBuffer}. 26 | * 27 | * @param request the request to encode. 28 | * @param buffer the buffer to encode into. 29 | */ 30 | public static void encode(WriteSingleRegisterRequest request, ByteBuffer buffer) { 31 | buffer.put((byte) request.getFunctionCode()); 32 | buffer.putShort((short) request.address); 33 | buffer.putShort((short) request.value); 34 | } 35 | 36 | /** 37 | * Decode a {@link WriteSingleRegisterRequest} from a {@link ByteBuffer}. 38 | * 39 | * @param buffer the buffer to decode from. 40 | * @return the decoded request. 41 | */ 42 | public static WriteSingleRegisterRequest decode(ByteBuffer buffer) { 43 | int functionCode = buffer.get() & 0xFF; 44 | assert functionCode == FunctionCode.WRITE_SINGLE_REGISTER.getCode(); 45 | 46 | int address = buffer.getShort() & 0xFFFF; 47 | int value = buffer.getShort() & 0xFFFF; 48 | 49 | return new WriteSingleRegisterRequest(address, value); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/WriteSingleRegisterResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#WRITE_SINGLE_REGISTER} response PDU. 8 | * 9 | * @param address the address of the register written to. 2 bytes, range [0x0000, 0xFFFF]. 10 | * @param value the value written. 2 bytes, range [0x0000, 0xFFFF]. 11 | */ 12 | public record WriteSingleRegisterResponse(int address, int value) implements ModbusResponsePdu { 13 | 14 | @Override 15 | public int getFunctionCode() { 16 | return FunctionCode.WRITE_SINGLE_REGISTER.getCode(); 17 | } 18 | 19 | /** Utility functions for encoding and decoding {@link WriteSingleRegisterResponse}. */ 20 | public static final class Serializer { 21 | 22 | private Serializer() {} 23 | 24 | /** 25 | * Encode a {@link WriteSingleRegisterResponse} into a {@link ByteBuffer}. 26 | * 27 | * @param response the response to encode. 28 | * @param buffer the buffer to encode into. 29 | */ 30 | public static void encode(WriteSingleRegisterResponse response, ByteBuffer buffer) { 31 | buffer.put((byte) response.getFunctionCode()); 32 | buffer.putShort((short) response.address); 33 | buffer.putShort((short) response.value); 34 | } 35 | 36 | /** 37 | * Decode a {@link WriteSingleRegisterResponse} from a {@link ByteBuffer}. 38 | * 39 | * @param buffer the buffer to decode from. 40 | * @return the decoded response. 41 | */ 42 | public static WriteSingleRegisterResponse decode(ByteBuffer buffer) { 43 | int functionCode = buffer.get(); 44 | assert functionCode == FunctionCode.WRITE_SINGLE_REGISTER.getCode(); 45 | 46 | int address = buffer.getShort() & 0xFFFF; 47 | int value = buffer.getShort() & 0xFFFF; 48 | 49 | return new WriteSingleRegisterResponse(address, value); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /modbus/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.digitalpetri.modbus 9 | modbus-parent 10 | 2.1.4-SNAPSHOT 11 | 12 | 13 | Modbus :: Core 14 | 15 | modbus 16 | 17 | 18 | com.digitalpetri.modbus 19 | 17 20 | 17 21 | UTF-8 22 | 23 | 24 | 25 | 26 | org.slf4j 27 | slf4j-api 28 | ${slf4j.version} 29 | 30 | 31 | 32 | org.junit.jupiter 33 | junit-jupiter-api 34 | test 35 | 36 | 37 | org.junit.jupiter 38 | junit-jupiter-engine 39 | test 40 | 41 | 42 | org.slf4j 43 | slf4j-simple 44 | test 45 | 46 | 47 | 48 | 49 | 50 | release 51 | 52 | 53 | 54 | org.sonatype.central 55 | central-publishing-maven-plugin 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/WriteMultipleRegistersResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#WRITE_MULTIPLE_REGISTERS} response PDU. 8 | * 9 | * @param address the starting address. 2 bytes, range [0x0000, 0xFFFF]. 10 | * @param quantity the quantity of registers written to. 2 bytes, range [0x0001, 0x007B]. 11 | */ 12 | public record WriteMultipleRegistersResponse(int address, int quantity) 13 | implements ModbusResponsePdu { 14 | 15 | @Override 16 | public int getFunctionCode() { 17 | return FunctionCode.WRITE_MULTIPLE_REGISTERS.getCode(); 18 | } 19 | 20 | /** Utility functions for encoding and decoding {@link WriteMultipleRegistersResponse}. */ 21 | public static class Serializer { 22 | 23 | private Serializer() {} 24 | 25 | /** 26 | * Encode a {@link WriteMultipleRegistersResponse} into a {@link ByteBuffer}. 27 | * 28 | * @param response the response to encode. 29 | * @param buffer the buffer to encode into. 30 | */ 31 | public static void encode(WriteMultipleRegistersResponse response, ByteBuffer buffer) { 32 | buffer.put((byte) response.getFunctionCode()); 33 | buffer.putShort((short) response.address); 34 | buffer.putShort((short) response.quantity); 35 | } 36 | 37 | /** 38 | * Decode a {@link WriteMultipleRegistersResponse} from a {@link ByteBuffer}. 39 | * 40 | * @param buffer the buffer to decode from. 41 | * @return the decoded response. 42 | */ 43 | public static WriteMultipleRegistersResponse decode(ByteBuffer buffer) { 44 | int functionCode = buffer.get() & 0xFF; 45 | assert functionCode == FunctionCode.WRITE_MULTIPLE_REGISTERS.getCode(); 46 | 47 | int address = buffer.getShort() & 0xFFFF; 48 | int quantity = buffer.getShort() & 0xFFFF; 49 | 50 | return new WriteMultipleRegistersResponse(address, quantity); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadCoilsRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#READ_COILS} request PDU. 8 | * 9 | *

Requests specify the starting address, i.e. the address of the first coil specified, and the 10 | * number of coils. In the PDU Coils are addressed starting at 0. 11 | * 12 | * @param address the starting address. 2 bytes, range [0x0000, 0xFFFF]. 13 | * @param quantity the quantity of coils to read. 2 bytes, range [0x0001, 0x07D0]. 14 | */ 15 | public record ReadCoilsRequest(int address, int quantity) implements ModbusRequestPdu { 16 | 17 | @Override 18 | public int getFunctionCode() { 19 | return FunctionCode.READ_COILS.getCode(); 20 | } 21 | 22 | /** Utility functions for encoding and decoding {@link ReadCoilsRequest}. */ 23 | public static final class Serializer { 24 | 25 | private Serializer() {} 26 | 27 | /** 28 | * Encode a {@link ReadCoilsRequest} into a {@link ByteBuffer}. 29 | * 30 | * @param request the request to encode. 31 | * @param buffer the buffer to encode into. 32 | */ 33 | public static void encode(ReadCoilsRequest request, ByteBuffer buffer) { 34 | buffer.put((byte) request.getFunctionCode()); 35 | buffer.putShort((short) request.address); 36 | buffer.putShort((short) request.quantity); 37 | } 38 | 39 | /** 40 | * Decode a {@link ReadCoilsRequest} from a {@link ByteBuffer}. 41 | * 42 | * @param buffer the buffer to decode from. 43 | * @return the decoded request. 44 | */ 45 | public static ReadCoilsRequest decode(ByteBuffer buffer) { 46 | int functionCode = buffer.get() & 0xFF; 47 | assert functionCode == FunctionCode.READ_COILS.getCode(); 48 | 49 | int address = buffer.getShort() & 0xFFFF; 50 | int quantity = buffer.getShort() & 0xFFFF; 51 | 52 | return new ReadCoilsRequest(address, quantity); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadDiscreteInputsRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#READ_DISCRETE_INPUTS} request PDU. 8 | * 9 | *

Requests specify the starting address, i.e. the address of the first input specified, and the 10 | * number of inputs. In the PDU inputs are addressed starting at 0. 11 | * 12 | * @param address the starting address. 2 bytes, range [0x0000, 0xFFFF]. 13 | * @param quantity the quantity of inputs to read. 2 bytes, range [0x0001, 0x07D0]. 14 | */ 15 | public record ReadDiscreteInputsRequest(int address, int quantity) implements ModbusRequestPdu { 16 | 17 | @Override 18 | public int getFunctionCode() { 19 | return FunctionCode.READ_DISCRETE_INPUTS.getCode(); 20 | } 21 | 22 | public static final class Serializer { 23 | 24 | private Serializer() {} 25 | 26 | /** 27 | * Encode a {@link ReadDiscreteInputsRequest} into a {@link ByteBuffer}. 28 | * 29 | * @param request the request to encode. 30 | * @param buffer the buffer to encode into. 31 | */ 32 | public static void encode(ReadDiscreteInputsRequest request, ByteBuffer buffer) { 33 | buffer.put((byte) request.getFunctionCode()); 34 | buffer.putShort((short) request.address); 35 | buffer.putShort((short) request.quantity); 36 | } 37 | 38 | /** 39 | * Decode a {@link ReadDiscreteInputsRequest} from a {@link ByteBuffer}. 40 | * 41 | * @param buffer the buffer to decode from. 42 | * @return the decoded request. 43 | */ 44 | public static ReadDiscreteInputsRequest decode(ByteBuffer buffer) { 45 | int functionCode = buffer.get() & 0xFF; 46 | assert functionCode == FunctionCode.READ_DISCRETE_INPUTS.getCode(); 47 | 48 | int address = buffer.getShort() & 0xFFFF; 49 | int quantity = buffer.getShort() & 0xFFFF; 50 | 51 | return new ReadDiscreteInputsRequest(address, quantity); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/WriteMultipleCoilsResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#WRITE_MULTIPLE_COILS} response PDU. 8 | * 9 | *

The normal response returns the function code, starting address, and quantity of coils forced. 10 | * 11 | * @param address the starting address. 2 bytes, range [0x0000, 0xFFFF]. 12 | * @param quantity the quantity of coils to write. 2 bytes, range [0x0001, 0x7B0]. 13 | */ 14 | public record WriteMultipleCoilsResponse(int address, int quantity) implements ModbusResponsePdu { 15 | 16 | @Override 17 | public int getFunctionCode() { 18 | return FunctionCode.WRITE_MULTIPLE_COILS.getCode(); 19 | } 20 | 21 | /** Utility functions for encoding and decoding {@link WriteMultipleCoilsResponse}. */ 22 | public static final class Serializer { 23 | 24 | private Serializer() {} 25 | 26 | /** 27 | * Encode a {@link WriteMultipleCoilsResponse} into a {@link ByteBuffer}. 28 | * 29 | * @param response the response to encode. 30 | * @param buffer the buffer to encode into. 31 | */ 32 | public static void encode(WriteMultipleCoilsResponse response, ByteBuffer buffer) { 33 | buffer.put((byte) response.getFunctionCode()); 34 | buffer.putShort((short) response.address); 35 | buffer.putShort((short) response.quantity); 36 | } 37 | 38 | /** 39 | * Decode a {@link WriteMultipleCoilsResponse} from a {@link ByteBuffer}. 40 | * 41 | * @param buffer the buffer to decode from. 42 | * @return the decoded response. 43 | */ 44 | public static WriteMultipleCoilsResponse decode(ByteBuffer buffer) { 45 | int functionCode = buffer.get() & 0xFF; 46 | assert functionCode == FunctionCode.WRITE_MULTIPLE_COILS.getCode(); 47 | 48 | int address = buffer.getShort() & 0xFFFF; 49 | int quantity = buffer.getShort() & 0xFFFF; 50 | 51 | return new WriteMultipleCoilsResponse(address, quantity); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/WriteSingleCoilResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#WRITE_SINGLE_COIL} response PDU. 8 | * 9 | *

The normal response is an echo of the request PDU, returned after the coil state has been 10 | * written. 11 | */ 12 | public record WriteSingleCoilResponse(int address, int value) implements ModbusResponsePdu { 13 | 14 | /** 15 | * @see #WriteSingleCoilResponse(int, int) 16 | */ 17 | public WriteSingleCoilResponse(int address, boolean value) { 18 | this(address, value ? 0xFF00 : 0x0000); 19 | } 20 | 21 | @Override 22 | public int getFunctionCode() { 23 | return FunctionCode.WRITE_SINGLE_COIL.getCode(); 24 | } 25 | 26 | /** Utility functions for encoding and decoding {@link WriteSingleCoilResponse}. */ 27 | public static final class Serializer { 28 | 29 | private Serializer() {} 30 | 31 | /** 32 | * Encode a {@link WriteSingleCoilResponse} into a {@link ByteBuffer}. 33 | * 34 | * @param response the response to encode. 35 | * @param buffer the buffer to encode into. 36 | */ 37 | public static void encode(WriteSingleCoilResponse response, ByteBuffer buffer) { 38 | buffer.put((byte) response.getFunctionCode()); 39 | buffer.putShort((short) response.address); 40 | buffer.putShort((short) response.value); 41 | } 42 | 43 | /** 44 | * Decode a {@link WriteSingleCoilResponse} from a {@link ByteBuffer}. 45 | * 46 | * @param buffer the buffer to decode from. 47 | * @return the decoded response. 48 | */ 49 | public static WriteSingleCoilResponse decode(ByteBuffer buffer) { 50 | int functionCode = buffer.get() & 0xFF; 51 | assert functionCode == FunctionCode.WRITE_SINGLE_COIL.getCode(); 52 | 53 | int address = buffer.getShort() & 0xFFFF; 54 | int value = buffer.getShort() & 0xFFFF; 55 | 56 | return new WriteSingleCoilResponse(address, value); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/MaskWriteRegisterRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#MASK_WRITE_REGISTER} request PDU. 8 | * 9 | * @param address the address, 2 bytes, range [0x0000, 0xFFFF]. 10 | * @param andMask the AND mask, 2 bytes, range [0x0000, 0xFFFF]. 11 | * @param orMask the OR mask, 2 bytes, range [0x0000, 0xFFFF]. 12 | */ 13 | public record MaskWriteRegisterRequest(int address, int andMask, int orMask) 14 | implements ModbusRequestPdu { 15 | 16 | @Override 17 | public int getFunctionCode() { 18 | return FunctionCode.MASK_WRITE_REGISTER.getCode(); 19 | } 20 | 21 | /** Utility functions for encoding and decoding {@link MaskWriteRegisterRequest}. */ 22 | public static class Serializer { 23 | 24 | private Serializer() {} 25 | 26 | /** 27 | * Encode a {@link MaskWriteRegisterRequest} into a {@link ByteBuffer}. 28 | * 29 | * @param request the request to encode. 30 | * @param buffer the buffer to encode into. 31 | */ 32 | public static void encode(MaskWriteRegisterRequest request, ByteBuffer buffer) { 33 | buffer.put((byte) request.getFunctionCode()); 34 | buffer.putShort((short) request.address); 35 | buffer.putShort((short) request.andMask); 36 | buffer.putShort((short) request.orMask); 37 | } 38 | 39 | /** 40 | * Decode a {@link MaskWriteRegisterRequest} from a {@link ByteBuffer}. 41 | * 42 | * @param buffer the buffer to decode from. 43 | * @return the decoded request. 44 | */ 45 | public static MaskWriteRegisterRequest decode(ByteBuffer buffer) { 46 | int functionCode = buffer.get() & 0xFF; 47 | assert functionCode == FunctionCode.MASK_WRITE_REGISTER.getCode(); 48 | 49 | int address = buffer.getShort() & 0xFFFF; 50 | int andMask = buffer.getShort() & 0xFFFF; 51 | int orMask = buffer.getShort() & 0xFFFF; 52 | 53 | return new MaskWriteRegisterRequest(address, andMask, orMask); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadInputRegistersRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#READ_INPUT_REGISTERS} request PDU. 8 | * 9 | *

Requests specify the starting register address and the number of register to read. In the PDU, 10 | * addresses are addressed starting at 0. 11 | * 12 | * @param address the starting address. 2 bytes, range [0x0000, 0xFFFF]. 13 | * @param quantity the quantity of registers to read. 2 bytes, range [0x01, 0x7D]. 14 | */ 15 | public record ReadInputRegistersRequest(int address, int quantity) implements ModbusRequestPdu { 16 | 17 | @Override 18 | public int getFunctionCode() { 19 | return FunctionCode.READ_INPUT_REGISTERS.getCode(); 20 | } 21 | 22 | /** Utility functions for encoding and decoding {@link ReadInputRegistersRequest}. */ 23 | public static final class Serializer { 24 | 25 | private Serializer() {} 26 | 27 | /** 28 | * Encode a {@link ReadInputRegistersRequest} into a {@link ByteBuffer}. 29 | * 30 | * @param request the request to encode. 31 | * @param buffer the buffer to encode into. 32 | */ 33 | public static void encode(ReadInputRegistersRequest request, ByteBuffer buffer) { 34 | buffer.put((byte) request.getFunctionCode()); 35 | buffer.putShort((short) request.address); 36 | buffer.putShort((short) request.quantity); 37 | } 38 | 39 | /** 40 | * Decode a {@link ReadInputRegistersRequest} from a {@link ByteBuffer}. 41 | * 42 | * @param buffer the buffer to decode from. 43 | * @return the decoded request. 44 | */ 45 | public static ReadInputRegistersRequest decode(ByteBuffer buffer) { 46 | int functionCode = buffer.get() & 0xFF; 47 | assert functionCode == FunctionCode.READ_INPUT_REGISTERS.getCode(); 48 | 49 | int address = buffer.getShort() & 0xFFFF; 50 | int quantity = buffer.getShort() & 0xFFFF; 51 | 52 | return new ReadInputRegistersRequest(address, quantity); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/MaskWriteRegisterResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#MASK_WRITE_REGISTER} response PDU. 8 | * 9 | * @param address the address, 2 bytes, range [0x0000, 0xFFFF]. 10 | * @param andMask the AND mask, 2 bytes, range [0x0000, 0xFFFF]. 11 | * @param orMask the OR mask, 2 bytes, range [0x0000, 0xFFFF]. 12 | */ 13 | public record MaskWriteRegisterResponse(int address, int andMask, int orMask) 14 | implements ModbusResponsePdu { 15 | 16 | @Override 17 | public int getFunctionCode() { 18 | return FunctionCode.MASK_WRITE_REGISTER.getCode(); 19 | } 20 | 21 | /** Utility functions for encoding and decoding {@link MaskWriteRegisterResponse}. */ 22 | public static final class Serializer { 23 | 24 | private Serializer() {} 25 | 26 | /** 27 | * Encode a {@link MaskWriteRegisterResponse} into a {@link ByteBuffer}. 28 | * 29 | * @param response the response to encode. 30 | * @param buffer the buffer to encode into. 31 | */ 32 | public static void encode(MaskWriteRegisterResponse response, ByteBuffer buffer) { 33 | buffer.put((byte) response.getFunctionCode()); 34 | buffer.putShort((short) response.address); 35 | buffer.putShort((short) response.andMask); 36 | buffer.putShort((short) response.orMask); 37 | } 38 | 39 | /** 40 | * Decode a {@link MaskWriteRegisterResponse} from a {@link ByteBuffer}. 41 | * 42 | * @param buffer the buffer to decode from. 43 | * @return the decoded response. 44 | */ 45 | public static MaskWriteRegisterResponse decode(ByteBuffer buffer) { 46 | int functionCode = buffer.get() & 0xFF; 47 | assert functionCode == FunctionCode.MASK_WRITE_REGISTER.getCode(); 48 | 49 | int address = buffer.getShort() & 0xFFFF; 50 | int andMask = buffer.getShort() & 0xFFFF; 51 | int orMask = buffer.getShort() & 0xFFFF; 52 | 53 | return new MaskWriteRegisterResponse(address, andMask, orMask); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadHoldingRegistersRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#READ_HOLDING_REGISTERS} request PDU. 8 | * 9 | *

Requests specify the starting register address and the number of register to read. In the PDU, 10 | * registers are addressed starting at 0. 11 | * 12 | * @param address the starting address. 2 bytes, range [0x0000, 0xFFFF]. 13 | * @param quantity the quantity of registers to read. 2 bytes, range [0x01, 0x7D]. 14 | */ 15 | public record ReadHoldingRegistersRequest(int address, int quantity) implements ModbusRequestPdu { 16 | 17 | @Override 18 | public int getFunctionCode() { 19 | return FunctionCode.READ_HOLDING_REGISTERS.getCode(); 20 | } 21 | 22 | /** Utility functions for encoding and decoding {@link ReadHoldingRegistersRequest}. */ 23 | public static final class Serializer { 24 | 25 | private Serializer() {} 26 | 27 | /** 28 | * Encode a {@link ReadHoldingRegistersRequest} into a {@link ByteBuffer}. 29 | * 30 | * @param request the request to encode. 31 | * @param buffer the buffer to encode into. 32 | */ 33 | public static void encode(ReadHoldingRegistersRequest request, ByteBuffer buffer) { 34 | buffer.put((byte) request.getFunctionCode()); 35 | buffer.putShort((short) request.address); 36 | buffer.putShort((short) request.quantity); 37 | } 38 | 39 | /** 40 | * Decode a {@link ReadHoldingRegistersRequest} from a {@link ByteBuffer}. 41 | * 42 | * @param buffer the buffer to decode from. 43 | * @return the decoded request. 44 | */ 45 | public static ReadHoldingRegistersRequest decode(ByteBuffer buffer) { 46 | int functionCode = buffer.get() & 0xFF; 47 | assert functionCode == FunctionCode.READ_HOLDING_REGISTERS.getCode(); 48 | 49 | int address = buffer.getShort() & 0xFFFF; 50 | int quantity = buffer.getShort() & 0xFFFF; 51 | 52 | return new ReadHoldingRegistersRequest(address, quantity); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/ModbusRtuRequestFrameParserTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import static com.digitalpetri.modbus.Util.partitions; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import com.digitalpetri.modbus.ModbusRtuRequestFrameParser.Accumulated; 7 | import com.digitalpetri.modbus.ModbusRtuRequestFrameParser.ParserState; 8 | import java.nio.ByteBuffer; 9 | import java.util.stream.Stream; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class ModbusRtuRequestFrameParserTest { 13 | 14 | private static final byte[] READ_COILS = 15 | new byte[] {0x01, 0x01, 0x00, 0x00, 0x00, 0x08, (byte) 0xCA, (byte) 0xFE}; 16 | 17 | private static final byte[] WRITE_MULTIPLE_REGISTERS = 18 | new byte[] { 19 | 0x01, 0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x01, 0x00, 0x02, (byte) 0x8B, (byte) 0x3A 20 | }; 21 | 22 | @Test 23 | void readCoils() { 24 | parseValidRequest(READ_COILS); 25 | } 26 | 27 | @Test 28 | void writeMultipleRegisters() { 29 | parseValidRequest(WRITE_MULTIPLE_REGISTERS); 30 | } 31 | 32 | private void parseValidRequest(byte[] validRequestData) { 33 | var parser = new ModbusRtuRequestFrameParser(); 34 | 35 | for (int i = 1; i <= validRequestData.length; i++) { 36 | parser.reset(); 37 | 38 | Stream chunks = partitions(validRequestData, i); 39 | 40 | chunks.forEach( 41 | chunk -> { 42 | ParserState state = parser.parse(chunk); 43 | System.out.println(state); 44 | }); 45 | System.out.println("--"); 46 | 47 | ParserState state = parser.getState(); 48 | if (state instanceof Accumulated a) { 49 | int expectedUnitId = validRequestData[0] & 0xFF; 50 | ByteBuffer expectedPdu = ByteBuffer.wrap(validRequestData, 1, validRequestData.length - 3); 51 | ByteBuffer expectedCrc = ByteBuffer.wrap(validRequestData, validRequestData.length - 2, 2); 52 | assertEquals(expectedUnitId, a.frame().unitId()); 53 | assertEquals(expectedPdu, a.frame().pdu()); 54 | assertEquals(expectedCrc, a.frame().crc()); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/client/ModbusRtuClientTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.client; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.digitalpetri.modbus.ModbusRtuFrame; 7 | import com.digitalpetri.modbus.exceptions.ModbusExecutionException; 8 | import com.digitalpetri.modbus.exceptions.ModbusTimeoutException; 9 | import com.digitalpetri.modbus.pdu.ReadHoldingRegistersRequest; 10 | import java.time.Duration; 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.concurrent.CompletionStage; 13 | import java.util.function.Consumer; 14 | import org.junit.jupiter.api.Test; 15 | 16 | public class ModbusRtuClientTest { 17 | 18 | @Test 19 | void timeoutHandleIsRemoved() throws ModbusExecutionException { 20 | var transport = new TimeoutRtuTransport(); 21 | var client = 22 | ModbusRtuClient.create(transport, cfg -> cfg.requestTimeout = Duration.ofMillis(100)); 23 | 24 | client.connect(); 25 | 26 | assertThrows( 27 | ModbusTimeoutException.class, 28 | () -> client.readHoldingRegisters(1, new ReadHoldingRegistersRequest(0, 10))); 29 | 30 | assertEquals(0, client.timeouts.size()); 31 | } 32 | 33 | private static class TimeoutRtuTransport implements ModbusRtuClientTransport { 34 | boolean connected = false; 35 | 36 | @Override 37 | public void resetFrameParser() {} 38 | 39 | @Override 40 | public CompletionStage connect() { 41 | connected = true; 42 | return CompletableFuture.completedFuture(null); 43 | } 44 | 45 | @Override 46 | public CompletionStage disconnect() { 47 | connected = false; 48 | return CompletableFuture.completedFuture(null); 49 | } 50 | 51 | @Override 52 | public boolean isConnected() { 53 | return connected; 54 | } 55 | 56 | @Override 57 | public CompletionStage send(ModbusRtuFrame frame) { 58 | return CompletableFuture.completedFuture(null); 59 | } 60 | 61 | @Override 62 | public void receive(Consumer frameReceiver) {} 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/WriteSingleCoilRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A {@link FunctionCode#WRITE_SINGLE_COIL} request PDU. 8 | * 9 | *

Requests specify the address of the coil to be forced. In the PDU, coils are addressed 10 | * starting at 0. 11 | * 12 | * @param address the address of the coil to force. 2 bytes, range [0x0000, 0xFFFF]. 13 | * @param value the value to force. 14 | */ 15 | public record WriteSingleCoilRequest(int address, int value) implements ModbusRequestPdu { 16 | 17 | /** 18 | * @see #WriteSingleCoilRequest(int, int) 19 | */ 20 | public WriteSingleCoilRequest(int address, boolean value) { 21 | this(address, value ? 0xFF00 : 0x0000); 22 | } 23 | 24 | @Override 25 | public int getFunctionCode() { 26 | return FunctionCode.WRITE_SINGLE_COIL.getCode(); 27 | } 28 | 29 | /** Utility functions for encoding and decoding {@link WriteSingleCoilRequest}. */ 30 | public static final class Serializer { 31 | 32 | private Serializer() {} 33 | 34 | /** 35 | * Encode a {@link WriteSingleCoilRequest} into a {@link ByteBuffer}. 36 | * 37 | * @param request the request to encode. 38 | * @param buffer the buffer to encode into. 39 | */ 40 | public static void encode(WriteSingleCoilRequest request, ByteBuffer buffer) { 41 | buffer.put((byte) request.getFunctionCode()); 42 | buffer.putShort((short) request.address); 43 | buffer.putShort((short) request.value); 44 | } 45 | 46 | /** 47 | * Decode a {@link WriteSingleCoilRequest} from a {@link ByteBuffer}. 48 | * 49 | * @param buffer the buffer to decode from. 50 | * @return the decoded request. 51 | */ 52 | public static WriteSingleCoilRequest decode(ByteBuffer buffer) { 53 | int functionCode = buffer.get() & 0xFF; 54 | assert functionCode == FunctionCode.WRITE_SINGLE_COIL.getCode(); 55 | 56 | int address = buffer.getShort() & 0xFFFF; 57 | int value = buffer.getShort() & 0xFFFF; 58 | 59 | return new WriteSingleCoilRequest(address, value); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /modbus-serial/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.digitalpetri.modbus 9 | modbus-parent 10 | 2.1.4-SNAPSHOT 11 | 12 | 13 | Modbus :: Serial 14 | 15 | modbus-serial 16 | 17 | 18 | com.digitalpetri.modbus.serial 19 | 17 20 | 17 21 | UTF-8 22 | 23 | 24 | 25 | 26 | com.digitalpetri.modbus 27 | modbus 28 | ${project.version} 29 | 30 | 31 | com.fazecast 32 | jSerialComm 33 | ${jserialcomm.version} 34 | 35 | 36 | 37 | org.junit.jupiter 38 | junit-jupiter-api 39 | test 40 | 41 | 42 | org.junit.jupiter 43 | junit-jupiter-engine 44 | test 45 | 46 | 47 | org.slf4j 48 | slf4j-simple 49 | test 50 | 51 | 52 | 53 | 54 | 55 | release 56 | 57 | 58 | 59 | org.sonatype.central 60 | central-publishing-maven-plugin 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadDiscreteInputsResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import com.digitalpetri.modbus.internal.util.Hex; 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import java.util.StringJoiner; 8 | 9 | /** A {@link FunctionCode#READ_DISCRETE_INPUTS} response PDU. */ 10 | public record ReadDiscreteInputsResponse(byte[] inputs) implements ModbusResponsePdu { 11 | 12 | @Override 13 | public int getFunctionCode() { 14 | return FunctionCode.READ_DISCRETE_INPUTS.getCode(); 15 | } 16 | 17 | @Override 18 | public boolean equals(Object o) { 19 | if (this == o) { 20 | return true; 21 | } 22 | if (o == null || getClass() != o.getClass()) { 23 | return false; 24 | } 25 | ReadDiscreteInputsResponse that = (ReadDiscreteInputsResponse) o; 26 | return Arrays.equals(inputs, that.inputs); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Arrays.hashCode(inputs); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | // note: overridden to give preferred representation of `inputs` bytes 37 | return new StringJoiner(", ", ReadDiscreteInputsResponse.class.getSimpleName() + "[", "]") 38 | .add("inputs=" + Hex.format(inputs)) 39 | .toString(); 40 | } 41 | 42 | /** Utility functions for encoding and decoding {@link ReadDiscreteInputsResponse}. */ 43 | public static final class Serializer { 44 | 45 | private Serializer() {} 46 | 47 | /** 48 | * Encode a {@link ReadDiscreteInputsResponse} into a {@link ByteBuffer}. 49 | * 50 | * @param response the response to encode. 51 | * @param buffer the buffer to encode into. 52 | */ 53 | public static void encode(ReadDiscreteInputsResponse response, ByteBuffer buffer) { 54 | buffer.put((byte) response.getFunctionCode()); 55 | buffer.put((byte) response.inputs.length); 56 | buffer.put(response.inputs); 57 | } 58 | 59 | /** 60 | * Decode a {@link ReadDiscreteInputsResponse} from a {@link ByteBuffer}. 61 | * 62 | * @param buffer the buffer to decode from. 63 | * @return the decoded response. 64 | */ 65 | public static ReadDiscreteInputsResponse decode(ByteBuffer buffer) { 66 | int functionCode = buffer.get() & 0xFF; 67 | assert functionCode == FunctionCode.READ_DISCRETE_INPUTS.getCode(); 68 | 69 | int byteCount = buffer.get() & 0xFF; 70 | var inputs = new byte[byteCount]; 71 | buffer.get(inputs); 72 | 73 | return new ReadDiscreteInputsResponse(inputs); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/ModbusServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server; 2 | 3 | import static com.digitalpetri.modbus.ModbusPduSerializer.DefaultRequestSerializer; 4 | import static com.digitalpetri.modbus.ModbusPduSerializer.DefaultResponseSerializer; 5 | 6 | import com.digitalpetri.modbus.ModbusPduSerializer; 7 | import java.util.function.Consumer; 8 | 9 | /** 10 | * Configuration for a {@link ModbusTcpServer}. 11 | * 12 | * @param requestSerializer the {@link ModbusPduSerializer} used to decode incoming requests. 13 | * @param responseSerializer the {@link ModbusPduSerializer} used to encode outgoing responses. 14 | */ 15 | public record ModbusServerConfig( 16 | ModbusPduSerializer requestSerializer, ModbusPduSerializer responseSerializer) { 17 | 18 | /** 19 | * Create a new {@link ModbusServerConfig} instance. 20 | * 21 | * @param configure a callback that accepts a {@link Builder} used to configure the new instance. 22 | * @return a new {@link ModbusServerConfig} instance. 23 | */ 24 | public static ModbusServerConfig create(Consumer configure) { 25 | var builder = new Builder(); 26 | configure.accept(builder); 27 | return builder.build(); 28 | } 29 | 30 | public static class Builder { 31 | 32 | /** The {@link ModbusPduSerializer} used to decode incoming requests. */ 33 | public ModbusPduSerializer requestSerializer = DefaultRequestSerializer.INSTANCE; 34 | 35 | /** The {@link ModbusPduSerializer} used to encode outgoing responses. */ 36 | public ModbusPduSerializer responseSerializer = DefaultResponseSerializer.INSTANCE; 37 | 38 | /** 39 | * Set the {@link ModbusPduSerializer} used to decode incoming requests. 40 | * 41 | * @param requestSerializer the request serializer. 42 | * @return this {@link Builder}. 43 | */ 44 | public Builder setRequestSerializer(ModbusPduSerializer requestSerializer) { 45 | this.requestSerializer = requestSerializer; 46 | return this; 47 | } 48 | 49 | /** 50 | * Set the {@link ModbusPduSerializer} used to encode outgoing responses. 51 | * 52 | * @param responseSerializer the response serializer. 53 | * @return this {@link Builder}. 54 | */ 55 | public Builder setResponseSerializer(ModbusPduSerializer responseSerializer) { 56 | this.responseSerializer = responseSerializer; 57 | return this; 58 | } 59 | 60 | /** 61 | * @return a new {@link ModbusServerConfig} instance. 62 | */ 63 | public ModbusServerConfig build() { 64 | return new ModbusServerConfig(requestSerializer, responseSerializer); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://img.shields.io/maven-central/v/com.digitalpetri.modbus/modbus.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.digitalpetri.modbus%22%20AND%20a%3A%22modbus%22) 2 | 3 | A modern, performant, easy to use client and server implementation of Modbus, supporting: 4 | - Modbus TCP 5 | - Modbus TCP Security (Modbus TCP with TLS) 6 | - Modbus RTU on Serial 7 | - Modbus RTU on TCP 8 | 9 | ### Quick Start Examples 10 | 11 | #### Modbus TCP Client 12 | ```java 13 | var transport = NettyTcpClientTransport.create(cfg -> { 14 | cfg.setHostname("172.17.0.2"); 15 | cfg.setPort(502); 16 | }); 17 | 18 | var client = ModbusTcpClient.create(transport); 19 | client.connect(); 20 | 21 | ReadHoldingRegistersResponse response = client.readHoldingRegisters( 22 | 1, 23 | new ReadHoldingRegistersRequest(0, 10) 24 | ); 25 | 26 | System.out.println("Response: " + response); 27 | ``` 28 | 29 | #### Modbus RTU on Serial Client 30 | ```java 31 | var transport = SerialPortClientTransport.create(cfg -> { 32 | cfg.setSerialPort("/dev/ttyUSB0"); 33 | cfg.setBaudRate(115200); 34 | cfg.setDataBits(8); 35 | cfg.setParity(SerialPort.NO_PARITY); 36 | cfg.setStopBits(SerialPort.TWO_STOP_BITS); 37 | }); 38 | 39 | var client = ModbusRtuClient.create(transport); 40 | client.connect(); 41 | 42 | client.readHoldingRegisters( 43 | 1, 44 | new ReadHoldingRegistersRequest(0, 10) 45 | ); 46 | 47 | System.out.println("Response: " + response); 48 | ``` 49 | 50 | ### Maven 51 | 52 | #### Modbus TCP 53 | 54 | ```xml 55 | 56 | com.digitalpetri.modbus 57 | modbus-tcp 58 | 2.1.3 59 | 60 | ``` 61 | 62 | #### Modbus Serial 63 | ```xml 64 | 65 | com.digitalpetri.modbus 66 | modbus-serial 67 | 2.1.3 68 | 69 | ``` 70 | 71 | ### Features 72 | 73 | #### Supported Function Codes 74 | Code | Function | Client | Server 75 | -------- | -------- | ------ | ------ 76 | 0x01 | Read Coils | ✅ | ✅ 77 | 0x02 | Read Discrete Inputs | ✅ | ✅ 78 | 0x03 | Read Holding Registers | ✅ | ✅ 79 | 0x04 | Read Input Registers | ✅ | ✅ 80 | 0x05 | Write Single Coil | ✅ | ✅ 81 | 0x06 | Write Single Register | ✅ | ✅ 82 | 0x0F | Write Multiple Coils | ✅ | ✅ 83 | 0x10 | Write Multiple Registers | ✅ | ✅ 84 | 0x16 | Mask Write Register | ✅ | ✅ 85 | 0x17 | Read/Write Multiple Registers | ✅ | ✅ 86 | 87 | - raw/custom PDUs on Modbus/TCP 88 | - broadcast messages on Modbus/RTU 89 | - pluggable codec implementations 90 | - pluggable transport implementations 91 | 92 | ### License 93 | 94 | Eclipse Public License - v 2.0 95 | -------------------------------------------------------------------------------- /modbus-tests/src/test/java/com/digitalpetri/modbus/test/ModbusRtuTcpClientServerIT.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.test; 2 | 3 | import com.digitalpetri.modbus.client.ModbusClient; 4 | import com.digitalpetri.modbus.client.ModbusRtuClient; 5 | import com.digitalpetri.modbus.server.ModbusRtuServer; 6 | import com.digitalpetri.modbus.server.ModbusServer; 7 | import com.digitalpetri.modbus.server.ProcessImage; 8 | import com.digitalpetri.modbus.server.ReadWriteModbusServices; 9 | import com.digitalpetri.modbus.tcp.client.NettyRtuClientTransport; 10 | import com.digitalpetri.modbus.tcp.server.NettyRtuServerTransport; 11 | import java.util.Optional; 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | 15 | public class ModbusRtuTcpClientServerIT extends ClientServerIT { 16 | 17 | ModbusRtuClient client; 18 | ModbusRtuServer server; 19 | 20 | @BeforeEach 21 | void setup() throws Exception { 22 | var processImage = new ProcessImage(); 23 | var modbusServices = 24 | new ReadWriteModbusServices() { 25 | @Override 26 | protected Optional getProcessImage(int unitId) { 27 | return Optional.of(processImage); 28 | } 29 | }; 30 | 31 | int serverPort = -1; 32 | 33 | for (int i = 50200; i < 65536; i++) { 34 | try { 35 | final var port = i; 36 | var serverTransport = 37 | NettyRtuServerTransport.create( 38 | cfg -> { 39 | cfg.bindAddress = "localhost"; 40 | cfg.port = port; 41 | }); 42 | 43 | System.out.println("trying port " + port); 44 | server = ModbusRtuServer.create(serverTransport, modbusServices); 45 | server.start(); 46 | serverPort = port; 47 | break; 48 | } catch (Exception e) { 49 | server = null; 50 | } 51 | } 52 | 53 | if (server == null) { 54 | throw new Exception("Failed to start server"); 55 | } 56 | 57 | final var port = serverPort; 58 | 59 | client = 60 | ModbusRtuClient.create( 61 | NettyRtuClientTransport.create( 62 | cfg -> { 63 | cfg.hostname = "localhost"; 64 | cfg.port = port; 65 | cfg.connectPersistent = false; 66 | })); 67 | client.connect(); 68 | } 69 | 70 | @AfterEach 71 | void teardown() throws Exception { 72 | if (client != null) { 73 | client.disconnect(); 74 | } 75 | if (server != null) { 76 | server.stop(); 77 | } 78 | } 79 | 80 | @Override 81 | ModbusClient getClient() { 82 | return client; 83 | } 84 | 85 | @Override 86 | ModbusServer getServer() { 87 | return server; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadInputRegistersResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import com.digitalpetri.modbus.internal.util.Hex; 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import java.util.StringJoiner; 8 | 9 | /** 10 | * A {@link FunctionCode#READ_INPUT_REGISTERS} response PDU. 11 | * 12 | *

The register data in the response is packed as 2 bytes per register. 13 | * 14 | * @param registers the register data, 2 bytes per register requested. 15 | */ 16 | public record ReadInputRegistersResponse(byte[] registers) implements ModbusResponsePdu { 17 | 18 | @Override 19 | public int getFunctionCode() { 20 | return FunctionCode.READ_INPUT_REGISTERS.getCode(); 21 | } 22 | 23 | @Override 24 | public boolean equals(Object o) { 25 | if (this == o) { 26 | return true; 27 | } 28 | if (o == null || getClass() != o.getClass()) { 29 | return false; 30 | } 31 | ReadInputRegistersResponse that = (ReadInputRegistersResponse) o; 32 | return Arrays.equals(registers, that.registers); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Arrays.hashCode(registers); 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | // note: overridden to give preferred representation of `registers` bytes 43 | return new StringJoiner(", ", ReadInputRegistersResponse.class.getSimpleName() + "[", "]") 44 | .add("registers=" + Hex.format(registers)) 45 | .toString(); 46 | } 47 | 48 | /** Utility functions for encoding and decoding {@link ReadInputRegistersResponse}. */ 49 | public static final class Serializer { 50 | 51 | private Serializer() {} 52 | 53 | /** 54 | * Encode a {@link ReadInputRegistersResponse} into a {@link ByteBuffer}. 55 | * 56 | * @param response the response to encode. 57 | * @param buffer the buffer to encode into. 58 | */ 59 | public static void encode(ReadInputRegistersResponse response, ByteBuffer buffer) { 60 | buffer.put((byte) response.getFunctionCode()); 61 | buffer.put((byte) response.registers.length); 62 | buffer.put(response.registers); 63 | } 64 | 65 | /** 66 | * Decode a {@link ReadInputRegistersResponse} from a {@link ByteBuffer}. 67 | * 68 | * @param buffer the buffer to decode from. 69 | * @return the decoded response. 70 | */ 71 | public static ReadInputRegistersResponse decode(ByteBuffer buffer) { 72 | int functionCode = buffer.get() & 0xFF; 73 | assert functionCode == FunctionCode.READ_INPUT_REGISTERS.getCode(); 74 | 75 | int byteCount = buffer.get() & 0xFF; 76 | var registers = new byte[byteCount]; 77 | buffer.get(registers); 78 | 79 | return new ReadInputRegistersResponse(registers); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadHoldingRegistersResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import com.digitalpetri.modbus.internal.util.Hex; 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import java.util.StringJoiner; 8 | 9 | /** 10 | * A {@link FunctionCode#READ_HOLDING_REGISTERS} response PDU. 11 | * 12 | *

The register data in the response is packed as 2 bytes per register. 13 | * 14 | * @param registers the register data, 2 bytes per register requested. 15 | */ 16 | public record ReadHoldingRegistersResponse(byte[] registers) implements ModbusResponsePdu { 17 | 18 | @Override 19 | public int getFunctionCode() { 20 | return FunctionCode.READ_HOLDING_REGISTERS.getCode(); 21 | } 22 | 23 | @Override 24 | public boolean equals(Object o) { 25 | if (this == o) { 26 | return true; 27 | } 28 | if (o == null || getClass() != o.getClass()) { 29 | return false; 30 | } 31 | ReadHoldingRegistersResponse response = (ReadHoldingRegistersResponse) o; 32 | return Arrays.equals(registers, response.registers); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Arrays.hashCode(registers); 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | // note: overridden to give preferred representation of `registers` bytes 43 | return new StringJoiner(", ", ReadHoldingRegistersResponse.class.getSimpleName() + "[", "]") 44 | .add("registers=" + Hex.format(registers)) 45 | .toString(); 46 | } 47 | 48 | /** Utility functions for encoding and decoding {@link ReadHoldingRegistersResponse}. */ 49 | public static final class Serializer { 50 | 51 | private Serializer() {} 52 | 53 | /** 54 | * Encode a {@link ReadHoldingRegistersResponse} into a {@link ByteBuffer}. 55 | * 56 | * @param response the response to encode. 57 | * @param buffer the buffer to encode into. 58 | */ 59 | public static void encode(ReadHoldingRegistersResponse response, ByteBuffer buffer) { 60 | buffer.put((byte) response.getFunctionCode()); 61 | buffer.put((byte) response.registers.length); 62 | buffer.put(response.registers); 63 | } 64 | 65 | /** 66 | * Decode a {@link ReadHoldingRegistersResponse} from a {@link ByteBuffer}. 67 | * 68 | * @param buffer the buffer to decode from. 69 | * @return the decoded response. 70 | */ 71 | public static ReadHoldingRegistersResponse decode(ByteBuffer buffer) { 72 | int functionCode = buffer.get() & 0xFF; 73 | assert functionCode == FunctionCode.READ_HOLDING_REGISTERS.getCode(); 74 | 75 | int byteCount = buffer.get() & 0xFF; 76 | var registers = new byte[byteCount]; 77 | buffer.get(registers); 78 | 79 | return new ReadHoldingRegistersResponse(registers); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadWriteMultipleRegistersResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import com.digitalpetri.modbus.internal.util.Hex; 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import java.util.StringJoiner; 8 | 9 | /** 10 | * A {@link FunctionCode#READ_WRITE_MULTIPLE_REGISTERS} response PDU. 11 | * 12 | * @param registers the register data, 2 bytes per register requested. 13 | */ 14 | public record ReadWriteMultipleRegistersResponse(byte[] registers) implements ModbusResponsePdu { 15 | 16 | @Override 17 | public int getFunctionCode() { 18 | return FunctionCode.READ_WRITE_MULTIPLE_REGISTERS.getCode(); 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) { 24 | return true; 25 | } 26 | if (o == null || getClass() != o.getClass()) { 27 | return false; 28 | } 29 | ReadWriteMultipleRegistersResponse response = (ReadWriteMultipleRegistersResponse) o; 30 | return Arrays.equals(registers, response.registers); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Arrays.hashCode(registers); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | // note: overridden to give preferred representation of `registers` bytes 41 | return new StringJoiner( 42 | ", ", ReadWriteMultipleRegistersResponse.class.getSimpleName() + "[", "]") 43 | .add("registers=" + Hex.format(registers)) 44 | .toString(); 45 | } 46 | 47 | /** Utility functions for encoding and decoding {@link ReadWriteMultipleRegistersResponse}. */ 48 | public static final class Serializer { 49 | 50 | private Serializer() {} 51 | 52 | /** 53 | * Encode a {@link ReadWriteMultipleRegistersResponse} into a {@link ByteBuffer}. 54 | * 55 | * @param response the response to encode. 56 | * @param buffer the buffer to encode into. 57 | */ 58 | public static void encode(ReadWriteMultipleRegistersResponse response, ByteBuffer buffer) { 59 | buffer.put((byte) response.getFunctionCode()); 60 | buffer.put((byte) response.registers.length); 61 | buffer.put(response.registers); 62 | } 63 | 64 | /** 65 | * Decode a {@link ReadWriteMultipleRegistersResponse} from a {@link ByteBuffer}. 66 | * 67 | * @param buffer the buffer to decode from. 68 | * @return the decoded response. 69 | */ 70 | public static ReadWriteMultipleRegistersResponse decode(ByteBuffer buffer) { 71 | int functionCode = buffer.get() & 0xFF; 72 | assert functionCode == FunctionCode.READ_WRITE_MULTIPLE_REGISTERS.getCode(); 73 | 74 | int byteCount = buffer.get() & 0xFF; 75 | var registers = new byte[byteCount]; 76 | buffer.get(registers); 77 | 78 | return new ReadWriteMultipleRegistersResponse(registers); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /modbus-tests/src/test/java/com/digitalpetri/modbus/test/ModbusRtuClientServerIT.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.test; 2 | 3 | import com.digitalpetri.modbus.client.ModbusClient; 4 | import com.digitalpetri.modbus.client.ModbusRtuClient; 5 | import com.digitalpetri.modbus.serial.client.SerialPortClientTransport; 6 | import com.digitalpetri.modbus.serial.server.SerialPortServerTransport; 7 | import com.digitalpetri.modbus.server.ModbusRtuServer; 8 | import com.digitalpetri.modbus.server.ModbusServer; 9 | import com.digitalpetri.modbus.server.ProcessImage; 10 | import com.digitalpetri.modbus.server.ReadWriteModbusServices; 11 | import com.fazecast.jSerialComm.SerialPort; 12 | import java.util.Optional; 13 | import org.junit.jupiter.api.AfterEach; 14 | import org.junit.jupiter.api.BeforeEach; 15 | import org.junit.jupiter.api.condition.EnabledIf; 16 | 17 | @EnabledIf("serialPortsConfigured") 18 | public class ModbusRtuClientServerIT extends ClientServerIT { 19 | 20 | ModbusRtuClient client; 21 | ModbusRtuServer server; 22 | 23 | @BeforeEach 24 | void setup() throws Exception { 25 | var processImage = new ProcessImage(); 26 | var modbusServices = 27 | new ReadWriteModbusServices() { 28 | @Override 29 | protected Optional getProcessImage(int unitId) { 30 | return Optional.of(processImage); 31 | } 32 | }; 33 | 34 | server = 35 | ModbusRtuServer.create( 36 | SerialPortServerTransport.create( 37 | cfg -> { 38 | cfg.serialPort = System.getProperty("modbus.serverSerialPort"); 39 | cfg.baudRate = 115200; 40 | cfg.dataBits = 8; 41 | cfg.parity = SerialPort.NO_PARITY; 42 | cfg.stopBits = 1; 43 | }), 44 | modbusServices); 45 | server.start(); 46 | 47 | client = 48 | ModbusRtuClient.create( 49 | SerialPortClientTransport.create( 50 | cfg -> { 51 | cfg.serialPort = System.getProperty("modbus.clientSerialPort"); 52 | cfg.baudRate = 115200; 53 | cfg.dataBits = 8; 54 | cfg.parity = SerialPort.NO_PARITY; 55 | cfg.stopBits = 1; 56 | })); 57 | client.connect(); 58 | } 59 | 60 | @AfterEach 61 | void teardown() throws Exception { 62 | if (client != null) { 63 | client.disconnect(); 64 | } 65 | if (server != null) { 66 | server.stop(); 67 | } 68 | } 69 | 70 | @Override 71 | ModbusClient getClient() { 72 | return client; 73 | } 74 | 75 | @Override 76 | ModbusServer getServer() { 77 | return server; 78 | } 79 | 80 | static boolean serialPortsConfigured() { 81 | return System.getProperty("modbus.clientSerialPort") != null 82 | && System.getProperty("modbus.serverSerialPort") != null; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadCoilsResponse.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import com.digitalpetri.modbus.internal.util.Hex; 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import java.util.StringJoiner; 8 | 9 | /** 10 | * A {@link FunctionCode#READ_COILS} response PDU. 11 | * 12 | *

The coils in the response message are packed as one coil per-bit. Status is indicated as 1=ON 13 | * and 0=OFF. 14 | * 15 | *

The LSB of the first data byte contains the output addressed in the query. The other coils 16 | * follow toward the high order end of this byte, and from low order to high order in subsequent 17 | * bytes. 18 | * 19 | *

If the returned output quantity is not a multiple of eight, the remaining bits in the last 20 | * byte will be padded with zeros (toward the high order end of the byte). 21 | * 22 | * @param coils the {@code byte[]} containing coil status, 8 coils per-byte. 23 | */ 24 | public record ReadCoilsResponse(byte[] coils) implements ModbusResponsePdu { 25 | 26 | @Override 27 | public int getFunctionCode() { 28 | return FunctionCode.READ_COILS.getCode(); 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (this == o) { 34 | return true; 35 | } 36 | if (o == null || getClass() != o.getClass()) { 37 | return false; 38 | } 39 | ReadCoilsResponse that = (ReadCoilsResponse) o; 40 | return Arrays.equals(coils, that.coils); 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | return Arrays.hashCode(coils); 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | // note: overridden to give preferred representation of `coils` bytes 51 | return new StringJoiner(", ", ReadCoilsResponse.class.getSimpleName() + "[", "]") 52 | .add("coils=" + Hex.format(coils)) 53 | .toString(); 54 | } 55 | 56 | /** Utility functions for encoding and decoding {@link ReadCoilsResponse}. */ 57 | public static final class Serializer { 58 | 59 | private Serializer() {} 60 | 61 | /** 62 | * Encode a {@link ReadCoilsResponse} into a {@link ByteBuffer}. 63 | * 64 | * @param response the response to encode. 65 | * @param buffer the buffer to encode into. 66 | */ 67 | public static void encode(ReadCoilsResponse response, ByteBuffer buffer) { 68 | buffer.put((byte) response.getFunctionCode()); 69 | buffer.put((byte) response.coils.length); 70 | buffer.put(response.coils); 71 | } 72 | 73 | /** 74 | * Decode a {@link ReadCoilsResponse} from a {@link ByteBuffer}. 75 | * 76 | * @param buffer the buffer to decode from. 77 | * @return the decoded response. 78 | */ 79 | public static ReadCoilsResponse decode(ByteBuffer buffer) { 80 | int functionCode = buffer.get() & 0xFF; 81 | assert functionCode == FunctionCode.READ_COILS.getCode(); 82 | 83 | int byteCount = buffer.get() & 0xFF; 84 | var coils = new byte[byteCount]; 85 | buffer.get(coils); 86 | 87 | return new ReadCoilsResponse(coils); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /modbus-tcp/src/main/java/com/digitalpetri/modbus/tcp/Netty.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.tcp; 2 | 3 | import io.netty.channel.nio.NioEventLoopGroup; 4 | import io.netty.util.HashedWheelTimer; 5 | import io.netty.util.Timeout; 6 | import java.util.concurrent.ThreadFactory; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.concurrent.atomic.AtomicLong; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public final class Netty { 12 | 13 | private static NioEventLoopGroup EVENT_LOOP; 14 | 15 | private static HashedWheelTimer WHEEL_TIMER; 16 | 17 | /** 18 | * @return a shared {@link NioEventLoopGroup}. 19 | */ 20 | public static synchronized NioEventLoopGroup sharedEventLoop() { 21 | if (EVENT_LOOP == null) { 22 | ThreadFactory threadFactory = 23 | new ThreadFactory() { 24 | private final AtomicLong threadNumber = new AtomicLong(0L); 25 | 26 | @Override 27 | public Thread newThread(Runnable r) { 28 | Thread thread = 29 | new Thread(r, "modbus-netty-event-loop-" + threadNumber.getAndIncrement()); 30 | thread.setDaemon(true); 31 | return thread; 32 | } 33 | }; 34 | 35 | EVENT_LOOP = new NioEventLoopGroup(1, threadFactory); 36 | } 37 | 38 | return EVENT_LOOP; 39 | } 40 | 41 | /** 42 | * @return a shared {@link HashedWheelTimer}. 43 | */ 44 | public static synchronized HashedWheelTimer sharedWheelTimer() { 45 | if (WHEEL_TIMER == null) { 46 | ThreadFactory threadFactory = 47 | r -> { 48 | Thread thread = new Thread(r, "modbus-netty-wheel-timer"); 49 | thread.setDaemon(true); 50 | return thread; 51 | }; 52 | 53 | WHEEL_TIMER = new HashedWheelTimer(threadFactory); 54 | } 55 | 56 | return WHEEL_TIMER; 57 | } 58 | 59 | /** 60 | * Release shared resources, waiting at most 5 seconds for each of the shared resources to shut 61 | * down gracefully. 62 | * 63 | * @see #releaseSharedResources(long, TimeUnit) 64 | */ 65 | public static synchronized void releaseSharedResources() { 66 | releaseSharedResources(5, TimeUnit.SECONDS); 67 | } 68 | 69 | /** 70 | * Release shared resources, waiting at most the specified timeout for each of the shared 71 | * resources to shut down gracefully. 72 | * 73 | * @param timeout the duration of the timeout. 74 | * @param unit the unit of the timeout duration. 75 | */ 76 | public static synchronized void releaseSharedResources(long timeout, TimeUnit unit) { 77 | if (EVENT_LOOP != null) { 78 | try { 79 | if (!EVENT_LOOP.shutdownGracefully().await(timeout, unit)) { 80 | LoggerFactory.getLogger(Netty.class) 81 | .warn("Event loop not shut down after {} {}.", timeout, unit); 82 | } 83 | } catch (InterruptedException e) { 84 | Thread.currentThread().interrupt(); 85 | LoggerFactory.getLogger(Netty.class).warn("Interrupted awaiting event loop shutdown", e); 86 | } 87 | EVENT_LOOP = null; 88 | } 89 | 90 | if (WHEEL_TIMER != null) { 91 | WHEEL_TIMER.stop().forEach(Timeout::cancel); 92 | WHEEL_TIMER = null; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /modbus-tcp/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.digitalpetri.modbus 9 | modbus-parent 10 | 2.1.4-SNAPSHOT 11 | 12 | 13 | Modbus :: TCP 14 | 15 | modbus-tcp 16 | 17 | 18 | com.digitalpetri.modbus.tcp 19 | 17 20 | 17 21 | UTF-8 22 | 23 | 24 | 25 | 26 | com.digitalpetri.modbus 27 | modbus 28 | ${project.version} 29 | 30 | 31 | io.netty 32 | netty-buffer 33 | ${netty.version} 34 | 35 | 36 | io.netty 37 | netty-codec 38 | ${netty.version} 39 | 40 | 41 | io.netty 42 | netty-handler 43 | ${netty.version} 44 | 45 | 46 | com.digitalpetri.netty 47 | netty-channel-fsm 48 | ${netty-channel-fsm.version} 49 | 50 | 51 | io.netty 52 | netty-handler 53 | 54 | 55 | org.slf4j 56 | slf4j-api 57 | 58 | 59 | 60 | 61 | org.slf4j 62 | slf4j-api 63 | ${slf4j.version} 64 | 65 | 66 | 67 | org.junit.jupiter 68 | junit-jupiter-api 69 | test 70 | 71 | 72 | org.junit.jupiter 73 | junit-jupiter-engine 74 | test 75 | 76 | 77 | org.slf4j 78 | slf4j-simple 79 | test 80 | 81 | 82 | 83 | 84 | 85 | release 86 | 87 | 88 | 89 | org.sonatype.central 90 | central-publishing-maven-plugin 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /modbus-tcp/src/main/java/com/digitalpetri/modbus/tcp/server/NettyRequestContext.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.tcp.server; 2 | 3 | import com.digitalpetri.modbus.server.ModbusRequestContext.ModbusRtuTlsRequestContext; 4 | import com.digitalpetri.modbus.server.ModbusRequestContext.ModbusTcpTlsRequestContext; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.handler.ssl.SslHandler; 7 | import io.netty.util.Attribute; 8 | import io.netty.util.AttributeKey; 9 | import java.net.SocketAddress; 10 | import java.security.cert.Certificate; 11 | import java.security.cert.X509Certificate; 12 | import java.util.Arrays; 13 | import java.util.Optional; 14 | import javax.net.ssl.SSLPeerUnverifiedException; 15 | 16 | /** 17 | * Combined {@link ModbusTcpTlsRequestContext} and {@link ModbusRtuTlsRequestContext} implementation 18 | * for Netty-based transports. 19 | */ 20 | class NettyRequestContext implements ModbusTcpTlsRequestContext, ModbusRtuTlsRequestContext { 21 | 22 | private static final AttributeKey CLIENT_ROLE = AttributeKey.valueOf("clientRole"); 23 | 24 | private static final AttributeKey CLIENT_CERTIFICATE_CHAIN = 25 | AttributeKey.valueOf("clientCertificateChain"); 26 | 27 | private final ChannelHandlerContext ctx; 28 | 29 | NettyRequestContext(ChannelHandlerContext ctx) { 30 | this.ctx = ctx; 31 | } 32 | 33 | @Override 34 | public SocketAddress localAddress() { 35 | return ctx.channel().localAddress(); 36 | } 37 | 38 | @Override 39 | public SocketAddress remoteAddress() { 40 | return ctx.channel().remoteAddress(); 41 | } 42 | 43 | @Override 44 | public Optional clientRole() { 45 | Attribute attr = ctx.channel().attr(CLIENT_ROLE); 46 | 47 | String clientRole = attr.get(); 48 | 49 | if (clientRole == null) { 50 | X509Certificate x509Certificate = clientCertificateChain()[0]; 51 | 52 | byte[] bs = x509Certificate.getExtensionValue("1.3.6.1.4.1.50316.802.1"); 53 | 54 | if (bs != null && bs.length >= 4) { 55 | // Strip the leading tag and length bytes. 56 | clientRole = new String(bs, 4, bs.length - 4); 57 | } else { 58 | clientRole = ""; 59 | } 60 | 61 | attr.set(clientRole); 62 | } 63 | 64 | if (clientRole.isEmpty()) { 65 | return Optional.empty(); 66 | } else { 67 | return Optional.of(clientRole); 68 | } 69 | } 70 | 71 | @Override 72 | public X509Certificate[] clientCertificateChain() { 73 | Attribute attr = ctx.channel().attr(CLIENT_CERTIFICATE_CHAIN); 74 | 75 | X509Certificate[] clientCertificateChain = attr.get(); 76 | 77 | if (clientCertificateChain == null) { 78 | try { 79 | SslHandler handler = ctx.channel().pipeline().get(SslHandler.class); 80 | Certificate[] peerCertificates = handler.engine().getSession().getPeerCertificates(); 81 | 82 | clientCertificateChain = 83 | Arrays.stream(peerCertificates) 84 | .map(cert -> (X509Certificate) cert) 85 | .toArray(X509Certificate[]::new); 86 | 87 | attr.set(clientCertificateChain); 88 | } catch (SSLPeerUnverifiedException e) { 89 | throw new RuntimeException(e); 90 | } 91 | } 92 | 93 | return clientCertificateChain; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /modbus-tcp/src/main/java/com/digitalpetri/modbus/tcp/security/SecurityUtil.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.tcp.security; 2 | 3 | import java.io.IOException; 4 | import java.security.GeneralSecurityException; 5 | import java.security.KeyStore; 6 | import java.security.PrivateKey; 7 | import java.security.cert.X509Certificate; 8 | import javax.net.ssl.KeyManagerFactory; 9 | import javax.net.ssl.TrustManagerFactory; 10 | 11 | public class SecurityUtil { 12 | 13 | /** 14 | * Create a {@link KeyManagerFactory} from a private key and certificates. 15 | * 16 | * @param privateKey the private key. 17 | * @param certificates the certificates. 18 | * @return a {@link KeyManagerFactory}. 19 | * @throws GeneralSecurityException if an error occurs. 20 | * @throws IOException if an error occurs. 21 | */ 22 | public static KeyManagerFactory createKeyManagerFactory( 23 | PrivateKey privateKey, X509Certificate... certificates) 24 | throws GeneralSecurityException, IOException { 25 | 26 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 27 | keyStore.load(null, null); 28 | 29 | keyStore.setKeyEntry("key", privateKey, new char[0], certificates); 30 | 31 | return createKeyManagerFactory(keyStore, new char[0]); 32 | } 33 | 34 | /** 35 | * Create a {@link KeyManagerFactory} from a {@link KeyStore}. 36 | * 37 | * @param keyStore the {@link KeyStore}. 38 | * @param keyStorePassword the password for the {@link KeyStore}. 39 | * @return a {@link KeyManagerFactory}. 40 | * @throws GeneralSecurityException if an error occurs. 41 | */ 42 | public static KeyManagerFactory createKeyManagerFactory( 43 | KeyStore keyStore, char[] keyStorePassword) throws GeneralSecurityException { 44 | 45 | KeyManagerFactory keyManagerFactory = 46 | KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 47 | 48 | keyManagerFactory.init(keyStore, keyStorePassword); 49 | 50 | return keyManagerFactory; 51 | } 52 | 53 | /** 54 | * Create a {@link TrustManagerFactory} from certificates. 55 | * 56 | * @param certificates the certificates. 57 | * @return a {@link TrustManagerFactory}. 58 | * @throws GeneralSecurityException if an error occurs. 59 | * @throws IOException if an error occurs. 60 | */ 61 | public static TrustManagerFactory createTrustManagerFactory(X509Certificate... certificates) 62 | throws GeneralSecurityException, IOException { 63 | 64 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 65 | keyStore.load(null, null); 66 | 67 | for (int i = 0; i < certificates.length; i++) { 68 | keyStore.setCertificateEntry("cert" + i, certificates[i]); 69 | } 70 | 71 | return createTrustManagerFactory(keyStore); 72 | } 73 | 74 | /** 75 | * Create a {@link TrustManagerFactory} from a {@link KeyStore}. 76 | * 77 | * @param keyStore the {@link KeyStore}. 78 | * @return a {@link TrustManagerFactory}. 79 | * @throws GeneralSecurityException if an error occurs. 80 | */ 81 | public static TrustManagerFactory createTrustManagerFactory(KeyStore keyStore) 82 | throws GeneralSecurityException { 83 | 84 | TrustManagerFactory trustManagerFactory = 85 | TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 86 | 87 | trustManagerFactory.init(keyStore); 88 | 89 | return trustManagerFactory; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/Crc16.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public class Crc16 { 6 | 7 | /** CRC-16/Modbus lookup table. */ 8 | private static final int[] TABLE = { 9 | 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 10 | 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, 11 | 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, 12 | 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, 13 | 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, 14 | 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, 15 | 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, 16 | 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 17 | 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, 18 | 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, 19 | 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, 20 | 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, 21 | 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, 22 | 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, 23 | 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, 24 | 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 25 | 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, 26 | 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, 27 | 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, 28 | 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, 29 | 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, 30 | 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, 31 | 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, 32 | 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 33 | 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, 34 | 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, 35 | 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 36 | 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, 37 | 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, 38 | 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, 39 | 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, 40 | 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 41 | }; 42 | 43 | private int sum = 0xFFFF; 44 | 45 | /** 46 | * @return the CRC-16 value. 47 | */ 48 | public int getValue() { 49 | return sum; 50 | } 51 | 52 | /** Resets the CRC-16 to the initial value. */ 53 | public void reset() { 54 | sum = 0xFFFF; 55 | } 56 | 57 | /** 58 | * Updates the CRC-16 with the given byte. 59 | * 60 | * @param b the byte to update the CRC-16 with. 61 | */ 62 | public void update(int b) { 63 | sum = (sum >> 8) ^ TABLE[((sum) ^ (b & 0xFF)) & 0xFF]; 64 | } 65 | 66 | /** 67 | * Updates the CRC-16 with the given {@link ByteBuffer}. 68 | * 69 | * @param buffer the {@link ByteBuffer} to update the CRC-16 with. 70 | */ 71 | public void update(ByteBuffer buffer) { 72 | int offset = buffer.position(); 73 | for (int i = offset; i < buffer.limit(); i++) { 74 | update(buffer.get(offset + i)); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/WriteMultipleCoilsRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import com.digitalpetri.modbus.internal.util.Hex; 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import java.util.Objects; 8 | import java.util.StringJoiner; 9 | 10 | /** 11 | * A {@link FunctionCode#WRITE_MULTIPLE_COILS} request PDU. 12 | * 13 | * @param address the starting address. 2 bytes, range [0x0000, 0xFFFF]. 14 | * @param quantity the quantity of coils to write. 2 bytes, range [0x0001, 0x7B0]. 15 | * @param values a buffer of at least N bytes, where N = (quantity + 7) / 8. 16 | */ 17 | public record WriteMultipleCoilsRequest(int address, int quantity, byte[] values) 18 | implements ModbusRequestPdu { 19 | 20 | @Override 21 | public int getFunctionCode() { 22 | return FunctionCode.WRITE_MULTIPLE_COILS.getCode(); 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) { 28 | return true; 29 | } 30 | if (o == null || getClass() != o.getClass()) { 31 | return false; 32 | } 33 | WriteMultipleCoilsRequest that = (WriteMultipleCoilsRequest) o; 34 | return Objects.equals(address, that.address) 35 | && Objects.equals(quantity, that.quantity) 36 | && Arrays.equals(values, that.values); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | int result = Objects.hash(address, quantity); 42 | result = 31 * result + Arrays.hashCode(values); 43 | return result; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | // note: overridden to give preferred representation of `values` bytes 49 | return new StringJoiner(", ", WriteMultipleCoilsRequest.class.getSimpleName() + "[", "]") 50 | .add("address=" + address) 51 | .add("quantity=" + quantity) 52 | .add("values=" + Hex.format(values)) 53 | .toString(); 54 | } 55 | 56 | /** Utility functions for encoding and decoding {@link WriteMultipleCoilsRequest}. */ 57 | public static final class Serializer { 58 | 59 | private Serializer() {} 60 | 61 | /** 62 | * Encode a {@link WriteMultipleCoilsRequest} into a {@link ByteBuffer}. 63 | * 64 | * @param request the request to encode. 65 | * @param buffer the buffer to encode into. 66 | */ 67 | public static void encode(WriteMultipleCoilsRequest request, ByteBuffer buffer) { 68 | buffer.put((byte) request.getFunctionCode()); 69 | buffer.putShort((short) request.address); 70 | buffer.putShort((short) request.quantity); 71 | 72 | int byteCount = (request.quantity + 7) / 8; 73 | buffer.put((byte) byteCount); 74 | buffer.put(request.values); 75 | } 76 | 77 | /** 78 | * Decode a {@link WriteMultipleCoilsRequest} from a {@link ByteBuffer}. 79 | * 80 | * @param buffer the buffer to decode from. 81 | * @return the decoded request. 82 | */ 83 | public static WriteMultipleCoilsRequest decode(ByteBuffer buffer) { 84 | int functionCode = buffer.get() & 0xFF; 85 | assert functionCode == FunctionCode.WRITE_MULTIPLE_COILS.getCode(); 86 | 87 | int address = buffer.getShort() & 0xFFFF; 88 | int quantity = buffer.getShort() & 0xFFFF; 89 | 90 | int byteCount = buffer.get() & 0xFF; 91 | var values = new byte[byteCount]; 92 | buffer.get(values); 93 | 94 | return new WriteMultipleCoilsRequest(address, quantity, values); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/internal/util/BufferPool.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.internal.util; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.Deque; 5 | import java.util.Map; 6 | import java.util.NavigableMap; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ConcurrentSkipListMap; 9 | import java.util.concurrent.LinkedBlockingDeque; 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | public abstract class BufferPool implements AutoCloseable { 13 | 14 | private static final int QUEUE_SIZE = 3; 15 | 16 | private final Map allocationCounts = new ConcurrentHashMap<>(); 17 | private final Map rejectionCounts = new ConcurrentHashMap<>(); 18 | 19 | private final NavigableMap> buffers = new ConcurrentSkipListMap<>(); 20 | 21 | public void give(ByteBuffer buffer) { 22 | Deque queue = 23 | buffers.computeIfAbsent(buffer.capacity(), k -> new LinkedBlockingDeque<>(QUEUE_SIZE)); 24 | if (!queue.offer(buffer)) { 25 | rejectionCounts.computeIfAbsent(buffer.capacity(), k -> new AtomicLong()).incrementAndGet(); 26 | } 27 | } 28 | 29 | public ByteBuffer take(int capacity) { 30 | var entry = buffers.ceilingEntry(capacity); 31 | 32 | if (entry != null) { 33 | Deque queue = entry.getValue(); 34 | ByteBuffer buffer = queue.poll(); 35 | 36 | if (buffer != null) { 37 | return buffer.clear().limit(capacity); 38 | } else { 39 | return allocate(capacity); 40 | } 41 | } else { 42 | return allocate(capacity); 43 | } 44 | } 45 | 46 | @Override 47 | public void close() { 48 | buffers.clear(); 49 | allocationCounts.clear(); 50 | rejectionCounts.clear(); 51 | } 52 | 53 | public Map getAllocationCounts() { 54 | return allocationCounts; 55 | } 56 | 57 | public Map getRejectionCounts() { 58 | return rejectionCounts; 59 | } 60 | 61 | protected final ByteBuffer allocate(int capacity) { 62 | allocationCounts.computeIfAbsent(capacity, k -> new AtomicLong()).incrementAndGet(); 63 | return create(capacity); 64 | } 65 | 66 | protected abstract ByteBuffer create(int capacity); 67 | 68 | public static class HeapBufferPool extends BufferPool { 69 | 70 | @Override 71 | protected ByteBuffer create(int capacity) { 72 | return ByteBuffer.allocate(capacity); 73 | } 74 | 75 | @Override 76 | public void give(ByteBuffer buffer) { 77 | assert !buffer.isDirect(); 78 | super.give(buffer); 79 | } 80 | } 81 | 82 | public static class DirectBufferPool extends BufferPool { 83 | 84 | @Override 85 | protected ByteBuffer create(int capacity) { 86 | return ByteBuffer.allocateDirect(capacity); 87 | } 88 | 89 | @Override 90 | public void give(ByteBuffer buffer) { 91 | assert buffer.isDirect(); 92 | super.give(buffer); 93 | } 94 | } 95 | 96 | public static class NoOpBufferPool extends BufferPool { 97 | 98 | @Override 99 | protected ByteBuffer create(int capacity) { 100 | return ByteBuffer.allocate(capacity); 101 | } 102 | 103 | @Override 104 | public void give(ByteBuffer buffer) {} 105 | 106 | @Override 107 | public ByteBuffer take(int capacity) { 108 | return allocate(capacity); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/WriteMultipleRegistersRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import com.digitalpetri.modbus.internal.util.Hex; 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import java.util.Objects; 8 | import java.util.StringJoiner; 9 | 10 | /** 11 | * A {@link FunctionCode#WRITE_MULTIPLE_REGISTERS} request PDU. 12 | * 13 | * @param address the starting address to write to. 2 bytes, range [0x0000, 0xFFFF]. 14 | * @param quantity the number of registers to write. 2 bytes, range [0x0001, 0x007B]. 15 | * @param values the values to write. Must be at least {@code 2 * quantity} bytes. 16 | */ 17 | public record WriteMultipleRegistersRequest(int address, int quantity, byte[] values) 18 | implements ModbusRequestPdu { 19 | 20 | @Override 21 | public int getFunctionCode() { 22 | return FunctionCode.WRITE_MULTIPLE_REGISTERS.getCode(); 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) { 28 | return true; 29 | } 30 | if (o == null || getClass() != o.getClass()) { 31 | return false; 32 | } 33 | WriteMultipleRegistersRequest that = (WriteMultipleRegistersRequest) o; 34 | return Objects.equals(address, that.address) 35 | && Objects.equals(quantity, that.quantity) 36 | && Arrays.equals(values, that.values); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | int result = Objects.hash(address, quantity); 42 | result = 31 * result + Arrays.hashCode(values); 43 | return result; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | // note: overridden to give preferred representation of `values` bytes 49 | return new StringJoiner(", ", WriteMultipleRegistersRequest.class.getSimpleName() + "[", "]") 50 | .add("address=" + address) 51 | .add("quantity=" + quantity) 52 | .add("values=" + Hex.format(values)) 53 | .toString(); 54 | } 55 | 56 | /** Utility functions for encoding and decoding {@link WriteMultipleRegistersRequest}. */ 57 | public static final class Serializer { 58 | 59 | private Serializer() {} 60 | 61 | /** 62 | * Encode a {@link WriteMultipleRegistersRequest} into a {@link ByteBuffer}. 63 | * 64 | * @param request the request to encode. 65 | * @param buffer the buffer to encode into. 66 | */ 67 | public static void encode(WriteMultipleRegistersRequest request, ByteBuffer buffer) { 68 | buffer.put((byte) request.getFunctionCode()); 69 | buffer.putShort((short) request.address); 70 | buffer.putShort((short) request.quantity); 71 | 72 | buffer.put((byte) request.values.length); 73 | buffer.put(request.values); 74 | } 75 | 76 | /** 77 | * Decode a {@link WriteMultipleRegistersRequest} from a {@link ByteBuffer}. 78 | * 79 | * @param buffer the buffer to decode from. 80 | * @return the decoded request. 81 | */ 82 | public static WriteMultipleRegistersRequest decode(ByteBuffer buffer) { 83 | int functionCode = buffer.get() & 0xFF; 84 | assert functionCode == FunctionCode.WRITE_MULTIPLE_REGISTERS.getCode(); 85 | 86 | int address = buffer.getShort() & 0xFFFF; 87 | int quantity = buffer.getShort() & 0xFFFF; 88 | int byteCount = buffer.get() & 0xFF; 89 | 90 | var values = new byte[byteCount]; 91 | buffer.get(values); 92 | 93 | return new WriteMultipleRegistersRequest(address, quantity, values); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/FunctionCode.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import java.util.Optional; 4 | 5 | public enum FunctionCode { 6 | 7 | /** Function Code 0x01 - Read Coils. */ 8 | READ_COILS(0x01), 9 | 10 | /** Function Code 0x02 - Read Discrete Inputs. */ 11 | READ_DISCRETE_INPUTS(0x02), 12 | 13 | /** Function Code 0x03 - Read Holding Registers. */ 14 | READ_HOLDING_REGISTERS(0x03), 15 | 16 | /** Function Code 0x04 - Read Input Registers. */ 17 | READ_INPUT_REGISTERS(0x04), 18 | 19 | /** Function Code 0x05 - Write Single Coil. */ 20 | WRITE_SINGLE_COIL(0x05), 21 | 22 | /** Function Code 0x06 - Write Single Register. */ 23 | WRITE_SINGLE_REGISTER(0x06), 24 | 25 | /** Function Code 0x07 - Read Exception Status. */ 26 | READ_EXCEPTION_STATUS(0x07), 27 | 28 | /** Function Code 0x08 - Diagnostics. */ 29 | DIAGNOSTICS(0x08), 30 | 31 | /** Function Code 0x0B - Get Comm Event Counter. */ 32 | GET_COMM_EVENT_COUNTER(0x0B), 33 | 34 | /** Function Code 0x0C - Get Comm Event Log. */ 35 | GET_COMM_EVENT_LOG(0x0C), 36 | 37 | /** Function Code 0x0F - Write Multiple Coils. */ 38 | WRITE_MULTIPLE_COILS(0x0F), 39 | 40 | /** Function Code 0x10 - Write Multiple Registers. */ 41 | WRITE_MULTIPLE_REGISTERS(0x10), 42 | 43 | /** Function Code 0x11 - Report Slave Id. */ 44 | REPORT_SLAVE_ID(0x11), 45 | 46 | /** Function Code 0x14 - Read File Record. */ 47 | READ_FILE_RECORD(0x14), 48 | 49 | /** Function Code 0x15 - Write File Record. */ 50 | WRITE_FILE_RECORD(0x15), 51 | 52 | /** Function Code 0x16 - Mask Write Register. */ 53 | MASK_WRITE_REGISTER(0x16), 54 | 55 | /** Function Code 0x17 - Read/Write Multiple Registers. */ 56 | READ_WRITE_MULTIPLE_REGISTERS(0x17), 57 | 58 | /** Function Code 0x18 - Read FIFO Queue. */ 59 | READ_FIFO_QUEUE(0x18), 60 | 61 | /** Function Code 0x2B - Encapsulated Interface Transport. */ 62 | ENCAPSULATED_INTERFACE_TRANSPORT(0x2B); 63 | 64 | FunctionCode(int code) { 65 | this.code = code; 66 | } 67 | 68 | private final int code; 69 | 70 | public int getCode() { 71 | return code; 72 | } 73 | 74 | /** 75 | * Look up the corresponding {@link FunctionCode} for {@code code}. 76 | * 77 | * @param code the function code to look up. 78 | * @return the corresponding {@link FunctionCode} for {@code code}. 79 | */ 80 | public static Optional from(int code) { 81 | FunctionCode fc = 82 | switch (code) { 83 | case 0x01 -> READ_COILS; 84 | case 0x02 -> READ_DISCRETE_INPUTS; 85 | case 0x03 -> READ_HOLDING_REGISTERS; 86 | case 0x04 -> READ_INPUT_REGISTERS; 87 | case 0x05 -> WRITE_SINGLE_COIL; 88 | case 0x06 -> WRITE_SINGLE_REGISTER; 89 | case 0x07 -> READ_EXCEPTION_STATUS; 90 | case 0x08 -> DIAGNOSTICS; 91 | case 0x0B -> GET_COMM_EVENT_COUNTER; 92 | case 0x0C -> GET_COMM_EVENT_LOG; 93 | case 0x0F -> WRITE_MULTIPLE_COILS; 94 | case 0x10 -> WRITE_MULTIPLE_REGISTERS; 95 | case 0x11 -> REPORT_SLAVE_ID; 96 | case 0x14 -> READ_FILE_RECORD; 97 | case 0x15 -> WRITE_FILE_RECORD; 98 | case 0x16 -> MASK_WRITE_REGISTER; 99 | case 0x17 -> READ_WRITE_MULTIPLE_REGISTERS; 100 | case 0x18 -> READ_FIFO_QUEUE; 101 | case 0x2B -> ENCAPSULATED_INTERFACE_TRANSPORT; 102 | default -> null; 103 | }; 104 | 105 | return Optional.ofNullable(fc); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/client/ModbusTcpClientTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.client; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.digitalpetri.modbus.MbapHeader; 7 | import com.digitalpetri.modbus.ModbusTcpFrame; 8 | import com.digitalpetri.modbus.exceptions.ModbusException; 9 | import java.nio.ByteBuffer; 10 | import java.util.concurrent.CompletableFuture; 11 | import java.util.concurrent.CompletionStage; 12 | import java.util.concurrent.ExecutionException; 13 | import java.util.function.Consumer; 14 | import org.junit.jupiter.api.Test; 15 | 16 | public class ModbusTcpClientTest { 17 | 18 | /** 19 | * Tests handling of an erroneous empty response PDU. 20 | * 21 | * @see https://github.com/digitalpetri/modbus/issues/121 23 | */ 24 | @Test 25 | void emptyResponsePdu() { 26 | var transport = new TestTransport(); 27 | var client = ModbusTcpClient.create(transport); 28 | 29 | CompletionStage cs = client.sendRawAsync(1, new byte[] {0x04, 0x03, 0x00, 0x00, 0x01}); 30 | 31 | transport.frameReceiver.accept( 32 | new ModbusTcpFrame(new MbapHeader(0, 1, 1, 1), ByteBuffer.allocate(0))); 33 | 34 | ExecutionException ex = 35 | assertThrows(ExecutionException.class, () -> cs.toCompletableFuture().get()); 36 | 37 | ModbusException cause = (ModbusException) ex.getCause(); 38 | assertEquals("empty response PDU", cause.getMessage()); 39 | } 40 | 41 | /** 42 | * Tests handling of a malformed exception response PDU containing only the function code | 0x80 43 | * and missing the required exception code byte. 44 | */ 45 | @Test 46 | void malformedExceptionResponsePdu() { 47 | var transport = new TestTransport(); 48 | var client = ModbusTcpClient.create(transport); 49 | 50 | // Send a request with function code 0x04 51 | CompletionStage cs = client.sendRawAsync(1, new byte[] {0x04, 0x03, 0x00, 0x00, 0x01}); 52 | 53 | // Receive a malformed exception response: only 1 byte (0x84), no exception code 54 | transport.frameReceiver.accept( 55 | new ModbusTcpFrame(new MbapHeader(0, 1, 1, 1), ByteBuffer.wrap(new byte[] {(byte) 0x84}))); 56 | 57 | ExecutionException ex = 58 | assertThrows(ExecutionException.class, () -> cs.toCompletableFuture().get()); 59 | 60 | ModbusException cause = (ModbusException) ex.getCause(); 61 | assertEquals("malformed exception response PDU: 84", cause.getMessage()); 62 | } 63 | 64 | private static class TestTransport implements ModbusTcpClientTransport { 65 | 66 | boolean connected = false; 67 | ModbusTcpFrame lastFrameSent; 68 | Consumer frameReceiver; 69 | 70 | @Override 71 | public CompletionStage connect() { 72 | connected = true; 73 | return CompletableFuture.completedFuture(null); 74 | } 75 | 76 | @Override 77 | public CompletionStage disconnect() { 78 | connected = false; 79 | return CompletableFuture.completedFuture(null); 80 | } 81 | 82 | @Override 83 | public boolean isConnected() { 84 | return connected; 85 | } 86 | 87 | @Override 88 | public CompletionStage send(ModbusTcpFrame frame) { 89 | lastFrameSent = frame; 90 | return CompletableFuture.completedFuture(null); 91 | } 92 | 93 | @Override 94 | public void receive(Consumer frameReceiver) { 95 | this.frameReceiver = frameReceiver; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /modbus/src/test/java/com/digitalpetri/modbus/ModbusRtuResponseFrameParserTest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import static com.digitalpetri.modbus.Util.partitions; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertInstanceOf; 6 | import static org.junit.jupiter.api.Assertions.fail; 7 | 8 | import com.digitalpetri.modbus.ModbusRtuResponseFrameParser.Accumulated; 9 | import com.digitalpetri.modbus.ModbusRtuResponseFrameParser.Accumulating; 10 | import com.digitalpetri.modbus.ModbusRtuResponseFrameParser.ParserState; 11 | import java.nio.ByteBuffer; 12 | import java.util.Arrays; 13 | import java.util.stream.Stream; 14 | import org.junit.jupiter.api.Test; 15 | 16 | class ModbusRtuResponseFrameParserTest { 17 | 18 | private static final byte[] READ_COILS = 19 | new byte[] {0x01, 0x01, 0x02, 0x01, 0x02, (byte) 0xCA, (byte) 0xFE}; 20 | 21 | private static final byte[] READ_HOLDING_REGISTERS = 22 | new byte[] {0x01, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, (byte) 0xCA, (byte) 0xFE}; 23 | 24 | @Test 25 | void readCoils() { 26 | parseValidResponse(READ_COILS); 27 | } 28 | 29 | @Test 30 | void readHoldingRegisters() { 31 | parseValidResponse(READ_HOLDING_REGISTERS); 32 | } 33 | 34 | @Test 35 | void readCoils_InvalidLength() { 36 | byte[] invalidLengthResponse = Arrays.copyOf(READ_COILS, READ_COILS.length); 37 | 38 | invalidLengthResponse[2] = (byte) (invalidLengthResponse[2] * 2); 39 | 40 | parseInvalidLengthResponse(invalidLengthResponse); 41 | } 42 | 43 | @Test 44 | void readHoldingRegisters_InvalidLength() { 45 | byte[] invalidLengthResponse = 46 | Arrays.copyOf(READ_HOLDING_REGISTERS, READ_HOLDING_REGISTERS.length); 47 | 48 | invalidLengthResponse[2] = (byte) (invalidLengthResponse[2] * 2); 49 | 50 | parseInvalidLengthResponse(invalidLengthResponse); 51 | } 52 | 53 | private void parseValidResponse(byte[] validResponseData) { 54 | var parser = new ModbusRtuResponseFrameParser(); 55 | 56 | for (int i = 1; i <= validResponseData.length; i++) { 57 | parser.reset(); 58 | 59 | partitions(validResponseData, i) 60 | .forEach( 61 | data -> { 62 | ParserState s = parser.parse(data); 63 | System.out.println(s); 64 | }); 65 | System.out.println("--"); 66 | 67 | ParserState state = parser.getState(); 68 | if (state instanceof Accumulated a) { 69 | int expectedUnitId = validResponseData[0] & 0xFF; 70 | ByteBuffer expectedPdu = 71 | ByteBuffer.wrap(validResponseData, 1, validResponseData.length - 3); 72 | ByteBuffer expectedCrc = 73 | ByteBuffer.wrap(validResponseData, validResponseData.length - 2, 2); 74 | assertEquals(expectedUnitId, a.frame().unitId()); 75 | assertEquals(expectedPdu, a.frame().pdu()); 76 | assertEquals(expectedCrc, a.frame().crc()); 77 | } else { 78 | fail("unexpected state: " + state); 79 | } 80 | } 81 | } 82 | 83 | private void parseInvalidLengthResponse(byte[] invalidLengthResponse) { 84 | var parser = new ModbusRtuResponseFrameParser(); 85 | 86 | for (int i = 1; i <= invalidLengthResponse.length; i++) { 87 | parser.reset(); 88 | 89 | Stream chunks = partitions(invalidLengthResponse, i); 90 | chunks.forEach( 91 | data -> { 92 | ParserState s = parser.parse(data); 93 | System.out.println(s); 94 | }); 95 | System.out.println("--"); 96 | 97 | ParserState state = parser.getState(); 98 | 99 | assertInstanceOf(Accumulating.class, state); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /modbus-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.digitalpetri.modbus 8 | modbus-parent 9 | 2.1.4-SNAPSHOT 10 | 11 | 12 | Modbus :: Tests 13 | 14 | modbus-tests 15 | 16 | 17 | com.digitalpetri.modbus.test 18 | 17 19 | 17 20 | UTF-8 21 | 22 | 23 | 24 | 25 | com.digitalpetri.modbus 26 | modbus-tcp 27 | ${project.version} 28 | 29 | 30 | com.digitalpetri.modbus 31 | modbus-serial 32 | ${project.version} 33 | 34 | 35 | 36 | org.junit.jupiter 37 | junit-jupiter-api 38 | test 39 | 40 | 41 | org.junit.jupiter 42 | junit-jupiter-engine 43 | test 44 | 45 | 46 | org.bouncycastle 47 | bcprov-jdk18on 48 | ${bouncycastle.version} 49 | test 50 | 51 | 52 | org.bouncycastle 53 | bcpkix-jdk18on 54 | ${bouncycastle.version} 55 | test 56 | 57 | 58 | org.slf4j 59 | slf4j-simple 60 | ${slf4j.version} 61 | test 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-compiler-plugin 70 | ${maven-compiler-plugin.version} 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-jar-plugin 75 | ${maven-jar-plugin.version} 76 | 77 | 78 | default-jar 79 | none 80 | 81 | 82 | 83 | 84 | maven-deploy-plugin 85 | ${maven-deploy-plugin.version} 86 | 87 | true 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-gpg-plugin 93 | ${maven-gpg-plugin.version} 94 | 95 | true 96 | 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-install-plugin 101 | ${maven-install-plugin.version} 102 | 103 | true 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /modbus-tests/src/test/java/com/digitalpetri/modbus/test/ModbusTcpTlsClientServerIT.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.test; 2 | 3 | import com.digitalpetri.modbus.client.ModbusClient; 4 | import com.digitalpetri.modbus.client.ModbusTcpClient; 5 | import com.digitalpetri.modbus.server.ModbusServer; 6 | import com.digitalpetri.modbus.server.ModbusTcpServer; 7 | import com.digitalpetri.modbus.server.ProcessImage; 8 | import com.digitalpetri.modbus.server.ReadWriteModbusServices; 9 | import com.digitalpetri.modbus.tcp.client.NettyTcpClientTransport; 10 | import com.digitalpetri.modbus.tcp.security.SecurityUtil; 11 | import com.digitalpetri.modbus.tcp.server.NettyTcpServerTransport; 12 | import com.digitalpetri.modbus.test.CertificateUtil.KeyPairCert; 13 | import com.digitalpetri.modbus.test.CertificateUtil.Role; 14 | import java.util.Optional; 15 | import javax.net.ssl.KeyManagerFactory; 16 | import javax.net.ssl.TrustManagerFactory; 17 | import org.junit.jupiter.api.BeforeEach; 18 | 19 | public class ModbusTcpTlsClientServerIT extends ClientServerIT { 20 | 21 | ModbusTcpClient client; 22 | ModbusTcpServer server; 23 | 24 | KeyPairCert authorityKeyPairCert = CertificateUtil.generateCaCertificate(); 25 | 26 | KeyPairCert clientKeyPairCert = 27 | CertificateUtil.generateCaSignedCertificate(Role.CLIENT, authorityKeyPairCert); 28 | 29 | KeyPairCert serverKeyPairCert = 30 | CertificateUtil.generateCaSignedCertificate(Role.SERVER, authorityKeyPairCert); 31 | 32 | NettyTcpClientTransport clientTransport; 33 | 34 | @BeforeEach 35 | void setup() throws Exception { 36 | var processImage = new ProcessImage(); 37 | var modbusServices = 38 | new ReadWriteModbusServices() { 39 | @Override 40 | protected Optional getProcessImage(int unitId) { 41 | return Optional.of(processImage); 42 | } 43 | }; 44 | 45 | KeyManagerFactory serverKeyManagerFactory = 46 | SecurityUtil.createKeyManagerFactory( 47 | serverKeyPairCert.keyPair().getPrivate(), serverKeyPairCert.certificate()); 48 | TrustManagerFactory serverTrustManagerFactory = 49 | SecurityUtil.createTrustManagerFactory(authorityKeyPairCert.certificate()); 50 | 51 | int serverPort = -1; 52 | 53 | for (int i = 50200; i < 65536; i++) { 54 | try { 55 | final var port = i; 56 | var serverTransport = 57 | NettyTcpServerTransport.create( 58 | cfg -> { 59 | cfg.bindAddress = "localhost"; 60 | cfg.port = port; 61 | 62 | cfg.tlsEnabled = true; 63 | cfg.keyManagerFactory = serverKeyManagerFactory; 64 | cfg.trustManagerFactory = serverTrustManagerFactory; 65 | }); 66 | 67 | System.out.println("trying port " + port); 68 | server = ModbusTcpServer.create(serverTransport, modbusServices); 69 | server.start(); 70 | serverPort = port; 71 | break; 72 | } catch (Exception e) { 73 | server = null; 74 | } 75 | } 76 | 77 | KeyManagerFactory clientKeyManagerFactory = 78 | SecurityUtil.createKeyManagerFactory( 79 | clientKeyPairCert.keyPair().getPrivate(), clientKeyPairCert.certificate()); 80 | TrustManagerFactory clientTrustManagerFactory = 81 | SecurityUtil.createTrustManagerFactory(authorityKeyPairCert.certificate()); 82 | 83 | final var port = serverPort; 84 | clientTransport = 85 | NettyTcpClientTransport.create( 86 | cfg -> { 87 | cfg.hostname = "localhost"; 88 | cfg.port = port; 89 | cfg.connectPersistent = false; 90 | 91 | cfg.tlsEnabled = true; 92 | cfg.keyManagerFactory = clientKeyManagerFactory; 93 | cfg.trustManagerFactory = clientTrustManagerFactory; 94 | }); 95 | 96 | client = ModbusTcpClient.create(clientTransport); 97 | client.connect(); 98 | } 99 | 100 | @Override 101 | ModbusClient getClient() { 102 | return client; 103 | } 104 | 105 | @Override 106 | ModbusServer getServer() { 107 | return server; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/client/ModbusClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.client; 2 | 3 | import static com.digitalpetri.modbus.ModbusPduSerializer.DefaultRequestSerializer; 4 | import static com.digitalpetri.modbus.ModbusPduSerializer.DefaultResponseSerializer; 5 | 6 | import com.digitalpetri.modbus.Modbus; 7 | import com.digitalpetri.modbus.ModbusPduSerializer; 8 | import com.digitalpetri.modbus.TimeoutScheduler; 9 | import java.time.Duration; 10 | import java.util.function.Consumer; 11 | 12 | /** 13 | * Configuration for a {@link ModbusClient}. 14 | * 15 | * @param requestTimeout the timeout duration for requests. 16 | * @param timeoutScheduler the {@link TimeoutScheduler} used to schedule request timeouts. 17 | * @param requestSerializer the {@link ModbusPduSerializer} used to encode requests. 18 | * @param responseSerializer the {@link ModbusPduSerializer} used to decode responses. 19 | */ 20 | public record ModbusClientConfig( 21 | Duration requestTimeout, 22 | TimeoutScheduler timeoutScheduler, 23 | ModbusPduSerializer requestSerializer, 24 | ModbusPduSerializer responseSerializer) { 25 | 26 | /** 27 | * Create a new {@link ModbusClientConfig} instance. 28 | * 29 | * @param configure a callback that accepts a {@link Builder} used to configure the new instance. 30 | * @return a new {@link ModbusClientConfig} instance. 31 | */ 32 | public static ModbusClientConfig create(Consumer configure) { 33 | var builder = new Builder(); 34 | configure.accept(builder); 35 | return builder.build(); 36 | } 37 | 38 | public static class Builder { 39 | 40 | /** The timeout duration for requests. */ 41 | public Duration requestTimeout = Duration.ofSeconds(5); 42 | 43 | /** The {@link TimeoutScheduler} used to schedule request timeouts. */ 44 | public TimeoutScheduler timeoutScheduler; 45 | 46 | /** The {@link ModbusPduSerializer} used to encode outgoing requests. */ 47 | public ModbusPduSerializer requestSerializer = DefaultRequestSerializer.INSTANCE; 48 | 49 | /** The {@link ModbusPduSerializer} used to decode incoming responses. */ 50 | public ModbusPduSerializer responseSerializer = DefaultResponseSerializer.INSTANCE; 51 | 52 | /** 53 | * Set the timeout duration for requests. 54 | * 55 | * @param requestTimeout the request timeout. 56 | * @return this {@link Builder}. 57 | */ 58 | public Builder setRequestTimeout(Duration requestTimeout) { 59 | this.requestTimeout = requestTimeout; 60 | return this; 61 | } 62 | 63 | /** 64 | * Set the {@link TimeoutScheduler} used to schedule request timeouts. 65 | * 66 | * @param timeoutScheduler the timeout scheduler. 67 | * @return this {@link Builder}. 68 | */ 69 | public Builder setTimeoutScheduler(TimeoutScheduler timeoutScheduler) { 70 | this.timeoutScheduler = timeoutScheduler; 71 | return this; 72 | } 73 | 74 | /** 75 | * Set the {@link ModbusPduSerializer} used to encode outgoing requests. 76 | * 77 | * @param requestSerializer the request serializer. 78 | * @return this {@link Builder}. 79 | */ 80 | public Builder setRequestSerializer(ModbusPduSerializer requestSerializer) { 81 | this.requestSerializer = requestSerializer; 82 | return this; 83 | } 84 | 85 | /** 86 | * Set the {@link ModbusPduSerializer} used to decode incoming responses. 87 | * 88 | * @param responseSerializer the response serializer. 89 | * @return this {@link Builder}. 90 | */ 91 | public Builder setResponseSerializer(ModbusPduSerializer responseSerializer) { 92 | this.responseSerializer = responseSerializer; 93 | return this; 94 | } 95 | 96 | /** 97 | * @return a new {@link ModbusClientConfig} instance. 98 | */ 99 | public ModbusClientConfig build() { 100 | if (timeoutScheduler == null) { 101 | timeoutScheduler = 102 | TimeoutScheduler.create(Modbus.sharedExecutor(), Modbus.sharedScheduledExecutor()); 103 | } 104 | 105 | return new ModbusClientConfig( 106 | requestTimeout, timeoutScheduler, requestSerializer, responseSerializer); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/pdu/ReadWriteMultipleRegistersRequest.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.pdu; 2 | 3 | import com.digitalpetri.modbus.FunctionCode; 4 | import com.digitalpetri.modbus.internal.util.Hex; 5 | import java.nio.ByteBuffer; 6 | import java.util.Arrays; 7 | import java.util.Objects; 8 | import java.util.StringJoiner; 9 | 10 | /** 11 | * A {@link FunctionCode#READ_WRITE_MULTIPLE_REGISTERS} request PDU. 12 | * 13 | * @param readAddress the starting address to read from. 2 bytes, range [0x0000, 0xFFFF]. 14 | * @param readQuantity the quantity of registers to read. 2 bytes, range [0x01, 0x7D]. 15 | * @param writeAddress the starting address to write to. 2 bytes, range [0x0000, 0xFFFF]. 16 | * @param writeQuantity the quantity of registers to write. 2 bytes, range [0x01, 0x79]. 17 | * @param values the register values to write. 2 bytes per register. 18 | */ 19 | public record ReadWriteMultipleRegistersRequest( 20 | int readAddress, int readQuantity, int writeAddress, int writeQuantity, byte[] values) 21 | implements ModbusRequestPdu { 22 | 23 | @Override 24 | public int getFunctionCode() { 25 | return FunctionCode.READ_WRITE_MULTIPLE_REGISTERS.getCode(); 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) { 31 | return true; 32 | } 33 | if (o == null || getClass() != o.getClass()) { 34 | return false; 35 | } 36 | ReadWriteMultipleRegistersRequest that = (ReadWriteMultipleRegistersRequest) o; 37 | return readAddress == that.readAddress 38 | && readQuantity == that.readQuantity 39 | && writeAddress == that.writeAddress 40 | && writeQuantity == that.writeQuantity 41 | && Objects.deepEquals(values, that.values); 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | return Objects.hash( 47 | readAddress, readQuantity, writeAddress, writeQuantity, Arrays.hashCode(values)); 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | // note: overridden to give preferred representation of `values` bytes 53 | return new StringJoiner( 54 | ", ", ReadWriteMultipleRegistersRequest.class.getSimpleName() + "[", "]") 55 | .add("readAddress=" + readAddress) 56 | .add("readQuantity=" + readQuantity) 57 | .add("writeAddress=" + writeAddress) 58 | .add("writeQuantity=" + writeQuantity) 59 | .add("values=" + Hex.format(values)) 60 | .toString(); 61 | } 62 | 63 | /** Utility functions for encoding and decoding {@link ReadWriteMultipleRegistersRequest}. */ 64 | public static final class Serializer { 65 | 66 | private Serializer() {} 67 | 68 | /** 69 | * Encode a {@link ReadWriteMultipleRegistersRequest} into a {@link ByteBuffer}. 70 | * 71 | * @param request the request to encode. 72 | * @param buffer the buffer to encode into. 73 | */ 74 | public static void encode(ReadWriteMultipleRegistersRequest request, ByteBuffer buffer) { 75 | buffer.put((byte) request.getFunctionCode()); 76 | buffer.putShort((short) request.readAddress); 77 | buffer.putShort((short) request.readQuantity); 78 | buffer.putShort((short) request.writeAddress); 79 | buffer.putShort((short) request.writeQuantity); 80 | buffer.put((byte) (2 * request.writeQuantity)); 81 | buffer.put(request.values); 82 | } 83 | 84 | /** 85 | * Decode a {@link ReadWriteMultipleRegistersRequest} from a {@link ByteBuffer}. 86 | * 87 | * @param buffer the buffer to decode from. 88 | * @return the decoded request. 89 | */ 90 | public static ReadWriteMultipleRegistersRequest decode(ByteBuffer buffer) { 91 | int functionCode = buffer.get() & 0xFF; 92 | assert functionCode == FunctionCode.READ_WRITE_MULTIPLE_REGISTERS.getCode(); 93 | 94 | int readAddress = buffer.getShort() & 0xFFFF; 95 | int readQuantity = buffer.getShort() & 0xFFFF; 96 | int writeAddress = buffer.getShort() & 0xFFFF; 97 | int writeQuantity = buffer.getShort() & 0xFFFF; 98 | int byteCount = buffer.get() & 0xFF; 99 | byte[] values = new byte[byteCount]; 100 | buffer.get(values); 101 | 102 | return new ReadWriteMultipleRegistersRequest( 103 | readAddress, readQuantity, writeAddress, writeQuantity, values); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/ExceptionCode.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import java.util.Optional; 4 | 5 | public enum ExceptionCode { 6 | 7 | /** 8 | * Exception Code 0x01 - Illegal Function. 9 | * 10 | *

The function code received in the query is not an allowable action for the server. 11 | */ 12 | ILLEGAL_FUNCTION(0x01), 13 | 14 | /** 15 | * Exception Code 0x02 - Illegal Data Address. 16 | * 17 | *

The data address received in the query is not an allowable address for the server. More 18 | * specifically, the combination of reference number and transfer length is invalid. 19 | */ 20 | ILLEGAL_DATA_ADDRESS(0x02), 21 | 22 | /** 23 | * Exception Code 0x03 - Illegal Data Value. 24 | * 25 | *

A value contained in the query data field is not an allowable value for server. 26 | * 27 | *

This indicates a fault in the structure of the remainder of a complex request, such as that 28 | * the implied length is incorrect. It specifically does NOT mean that a data item submitted for 29 | * storage in a register has a value outside the expectation of the application program. 30 | */ 31 | ILLEGAL_DATA_VALUE(0x03), 32 | 33 | /** 34 | * Exception Code 0x04 - Slave Device Failure. 35 | * 36 | *

An unrecoverable error occurred while the server was attempting to perform the requested 37 | * action. 38 | */ 39 | SLAVE_DEVICE_FAILURE(0x04), 40 | 41 | /** 42 | * Exception Code 0x05 - Acknowledge. 43 | * 44 | *

Specialized use in conjunction with programming commands. 45 | * 46 | *

The server has accepted the request and is processing it, but a long duration of time will 47 | * be required to do so. This response is returned to prevent a timeout error from occurring in 48 | * the client. 49 | */ 50 | ACKNOWLEDGE(0x05), 51 | 52 | /** 53 | * Exception Code 0x06 - Slave Device Busy. 54 | * 55 | *

Specialized use in conjunction with programming commands. 56 | * 57 | *

The server is engaged in processing a long–duration program command. The client should 58 | * retransmit the message later when the server is free. 59 | */ 60 | SLAVE_DEVICE_BUSY(0x06), 61 | 62 | /** 63 | * Exception Code 0x08 - Memory Parity Error. 64 | * 65 | *

Specialized use in conjunction with function codes 20 and 21 and reference type 6, to 66 | * indicate that the extended file area failed to pass a consistency check. 67 | */ 68 | MEMORY_PARITY_ERROR(0x08), 69 | 70 | /** 71 | * Exception Code 0x0A - Gateway Path Unavailable. 72 | * 73 | *

Specialized use in conjunction with gateways, indicates that the gateway was unable to 74 | * allocate an internal communication path from the input port to the output port for processing 75 | * the request. Usually means that the gateway is misconfigured or overloaded. 76 | */ 77 | GATEWAY_PATH_UNAVAILABLE(0x0A), 78 | 79 | /** 80 | * Exception Code 0x0B - Gateway Target Device Failed to Respond. 81 | * 82 | *

Specialized use in conjunction with gateways, indicates that no response was obtained from 83 | * the target device. Usually means that the device is not present on the network. 84 | */ 85 | GATEWAY_TARGET_DEVICE_FAILED_TO_RESPONSE(0x0B); 86 | 87 | private final int code; 88 | 89 | ExceptionCode(int code) { 90 | this.code = code; 91 | } 92 | 93 | public int getCode() { 94 | return code; 95 | } 96 | 97 | /** 98 | * Look up the corresponding {@link ExceptionCode} for {@code code}. 99 | * 100 | * @param code the exception code to look up. 101 | * @return the corresponding {@link ExceptionCode} for {@code code}. 102 | */ 103 | public static Optional from(int code) { 104 | ExceptionCode ec = 105 | switch (code) { 106 | case 0x01 -> ILLEGAL_FUNCTION; 107 | case 0x02 -> ILLEGAL_DATA_ADDRESS; 108 | case 0x03 -> ILLEGAL_DATA_VALUE; 109 | case 0x04 -> SLAVE_DEVICE_FAILURE; 110 | case 0x05 -> ACKNOWLEDGE; 111 | case 0x06 -> SLAVE_DEVICE_BUSY; 112 | case 0x08 -> MEMORY_PARITY_ERROR; 113 | case 0x0A -> GATEWAY_PATH_UNAVAILABLE; 114 | case 0x0B -> GATEWAY_TARGET_DEVICE_FAILED_TO_RESPONSE; 115 | default -> null; 116 | }; 117 | 118 | return Optional.ofNullable(ec); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/internal/util/ExecutionQueue.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.internal.util; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.concurrent.Executor; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | /** 9 | * Queues up submitted {@link Runnable}s and executes on an {@link Executor}, with optional 10 | * concurrency. 11 | * 12 | *

When {@code concurrency = 1} (the default) submitted tasks are guaranteed to run serially and 13 | * in the order submitted. 14 | * 15 | *

When {@code concurrency > 1} there are no guarantees beyond the fact that tasks are still 16 | * pulled from a queue to be executed. 17 | */ 18 | public class ExecutionQueue { 19 | 20 | private static final Logger LOGGER = LoggerFactory.getLogger(ExecutionQueue.class); 21 | 22 | private final Object queueLock = new Object(); 23 | private final ArrayDeque queue = new ArrayDeque<>(); 24 | 25 | private int pending = 0; 26 | private boolean paused = false; 27 | 28 | private final Executor executor; 29 | private final int concurrencyLimit; 30 | 31 | public ExecutionQueue(Executor executor) { 32 | this(executor, 1); 33 | } 34 | 35 | public ExecutionQueue(Executor executor, int concurrencyLimit) { 36 | this.executor = executor; 37 | this.concurrencyLimit = concurrencyLimit; 38 | } 39 | 40 | /** 41 | * Submit a {@link Runnable} to be executed. 42 | * 43 | * @param runnable the {@link Runnable} to be executed. 44 | */ 45 | public void submit(Runnable runnable) { 46 | synchronized (queueLock) { 47 | queue.add(runnable); 48 | 49 | maybePollAndExecute(); 50 | } 51 | } 52 | 53 | /** 54 | * Submit a {@link Runnable} to be executed at the head of the queue. 55 | * 56 | * @param runnable the {@link Runnable} to be executed. 57 | */ 58 | public void submitToHead(Runnable runnable) { 59 | synchronized (queueLock) { 60 | queue.addFirst(runnable); 61 | 62 | maybePollAndExecute(); 63 | } 64 | } 65 | 66 | /** Pause execution of queued {@link Runnable}s. */ 67 | public void pause() { 68 | synchronized (queueLock) { 69 | paused = true; 70 | } 71 | } 72 | 73 | /** Resume execution of queued {@link Runnable}s. */ 74 | public void resume() { 75 | synchronized (queueLock) { 76 | paused = false; 77 | 78 | maybePollAndExecute(); 79 | } 80 | } 81 | 82 | private void maybePollAndExecute() { 83 | synchronized (queueLock) { 84 | if (pending < concurrencyLimit && !paused && !queue.isEmpty()) { 85 | executor.execute(new Task(queue.poll())); 86 | pending++; 87 | } 88 | } 89 | } 90 | 91 | private class Task implements Runnable { 92 | 93 | private final Runnable runnable; 94 | 95 | Task(Runnable runnable) { 96 | if (runnable == null) { 97 | throw new NullPointerException("runnable"); 98 | } 99 | 100 | this.runnable = runnable; 101 | } 102 | 103 | @Override 104 | public void run() { 105 | try { 106 | runnable.run(); 107 | } catch (Throwable throwable) { 108 | LOGGER.warn("Uncaught Throwable during execution", throwable); 109 | } 110 | 111 | InlineTask inlineTask = null; 112 | 113 | synchronized (queueLock) { 114 | if (queue.isEmpty() || paused) { 115 | pending--; 116 | } else { 117 | // pending count remains the same 118 | inlineTask = new InlineTask(queue.poll()); 119 | } 120 | } 121 | 122 | if (inlineTask != null) { 123 | inlineTask.run(); 124 | } 125 | } 126 | } 127 | 128 | private class InlineTask implements Runnable { 129 | 130 | private final Runnable runnable; 131 | 132 | InlineTask(Runnable runnable) { 133 | if (runnable == null) { 134 | throw new NullPointerException("runnable"); 135 | } 136 | 137 | this.runnable = runnable; 138 | } 139 | 140 | @Override 141 | public void run() { 142 | try { 143 | runnable.run(); 144 | } catch (Throwable throwable) { 145 | LOGGER.warn("Uncaught Throwable during execution", throwable); 146 | } 147 | 148 | synchronized (queueLock) { 149 | if (queue.isEmpty() || paused) { 150 | pending--; 151 | } else { 152 | // pending count remains the same 153 | executor.execute(new Task(queue.poll())); 154 | } 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/ReadOnlyModbusServices.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server; 2 | 3 | import com.digitalpetri.modbus.exceptions.UnknownUnitIdException; 4 | import com.digitalpetri.modbus.pdu.ReadCoilsRequest; 5 | import com.digitalpetri.modbus.pdu.ReadCoilsResponse; 6 | import com.digitalpetri.modbus.pdu.ReadDiscreteInputsRequest; 7 | import com.digitalpetri.modbus.pdu.ReadDiscreteInputsResponse; 8 | import com.digitalpetri.modbus.pdu.ReadHoldingRegistersRequest; 9 | import com.digitalpetri.modbus.pdu.ReadHoldingRegistersResponse; 10 | import com.digitalpetri.modbus.pdu.ReadInputRegistersRequest; 11 | import com.digitalpetri.modbus.pdu.ReadInputRegistersResponse; 12 | import java.util.Map; 13 | import java.util.Optional; 14 | import java.util.function.Function; 15 | 16 | public abstract class ReadOnlyModbusServices implements ModbusServices { 17 | 18 | protected abstract Optional getProcessImage(int unitId); 19 | 20 | @Override 21 | public ReadCoilsResponse readCoils( 22 | ModbusRequestContext context, int unitId, ReadCoilsRequest request) 23 | throws UnknownUnitIdException { 24 | 25 | ProcessImage processImage = 26 | getProcessImage(unitId).orElseThrow(() -> new UnknownUnitIdException(unitId)); 27 | 28 | final int address = request.address(); 29 | final int quantity = request.quantity(); 30 | 31 | byte[] coils = processImage.get(tx -> tx.readCoils(readBits(address, quantity))); 32 | 33 | return new ReadCoilsResponse(coils); 34 | } 35 | 36 | @Override 37 | public ReadDiscreteInputsResponse readDiscreteInputs( 38 | ModbusRequestContext context, int unitId, ReadDiscreteInputsRequest request) 39 | throws UnknownUnitIdException { 40 | 41 | ProcessImage processImage = 42 | getProcessImage(unitId).orElseThrow(() -> new UnknownUnitIdException(unitId)); 43 | 44 | final int address = request.address(); 45 | final int quantity = request.quantity(); 46 | 47 | byte[] inputs = processImage.get(tx -> tx.readDiscreteInputs(readBits(address, quantity))); 48 | 49 | return new ReadDiscreteInputsResponse(inputs); 50 | } 51 | 52 | @Override 53 | public ReadHoldingRegistersResponse readHoldingRegisters( 54 | ModbusRequestContext context, int unitId, ReadHoldingRegistersRequest request) 55 | throws UnknownUnitIdException { 56 | 57 | ProcessImage processImage = 58 | getProcessImage(unitId).orElseThrow(() -> new UnknownUnitIdException(unitId)); 59 | 60 | final int address = request.address(); 61 | final int quantity = request.quantity(); 62 | 63 | byte[] registers = 64 | processImage.get(tx -> tx.readHoldingRegisters(readRegisters(address, quantity))); 65 | 66 | return new ReadHoldingRegistersResponse(registers); 67 | } 68 | 69 | @Override 70 | public ReadInputRegistersResponse readInputRegisters( 71 | ModbusRequestContext context, int unitId, ReadInputRegistersRequest request) 72 | throws UnknownUnitIdException { 73 | 74 | ProcessImage processImage = 75 | getProcessImage(unitId).orElseThrow(() -> new UnknownUnitIdException(unitId)); 76 | 77 | final int address = request.address(); 78 | final int quantity = request.quantity(); 79 | 80 | byte[] registers = 81 | processImage.get(tx -> tx.readInputRegisters(readRegisters(address, quantity))); 82 | 83 | return new ReadInputRegistersResponse(registers); 84 | } 85 | 86 | private static Function, byte[]> readBits(int address, int quantity) { 87 | final var bytes = new byte[(quantity + 7) / 8]; 88 | 89 | return bitMap -> { 90 | for (int i = 0; i < quantity; i++) { 91 | int bitIndex = i % 8; 92 | int byteIndex = i / 8; 93 | 94 | boolean value = bitMap.getOrDefault(address + i, false); 95 | 96 | int b = bytes[byteIndex]; 97 | if (value) { 98 | b |= (1 << bitIndex); 99 | } else { 100 | b &= ~(1 << bitIndex); 101 | } 102 | bytes[byteIndex] = (byte) (b & 0xFF); 103 | } 104 | 105 | return bytes; 106 | }; 107 | } 108 | 109 | protected static Function, byte[]> readRegisters(int address, int quantity) { 110 | final var registers = new byte[quantity * 2]; 111 | 112 | return registerMap -> { 113 | for (int i = 0; i < quantity; i++) { 114 | byte[] value = registerMap.getOrDefault(address + i, new byte[2]); 115 | 116 | registers[i * 2] = value[0]; 117 | registers[i * 2 + 1] = value[1]; 118 | } 119 | 120 | return registers; 121 | }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/ModbusRtuRequestFrameParser.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.concurrent.atomic.AtomicReference; 5 | 6 | public class ModbusRtuRequestFrameParser { 7 | 8 | private final AtomicReference state = new AtomicReference<>(new Idle()); 9 | 10 | /** 11 | * Parse incoming data and return the updated {@link ParserState}. 12 | * 13 | * @param data the incoming data to parse. 14 | * @return the updated {@link ParserState}. 15 | */ 16 | public ParserState parse(byte[] data) { 17 | return state.updateAndGet(s -> s.parse(data)); 18 | } 19 | 20 | /** 21 | * Get the current {@link ParserState}. 22 | * 23 | * @return the current {@link ParserState}. 24 | */ 25 | public ParserState getState() { 26 | return state.get(); 27 | } 28 | 29 | /** Reset this parser to the {@link Idle} state. */ 30 | public ParserState reset() { 31 | return state.getAndSet(new Idle()); 32 | } 33 | 34 | public sealed interface ParserState permits Idle, Accumulating, Accumulated, ParseError { 35 | 36 | ParserState parse(byte[] data); 37 | } 38 | 39 | /** Waiting to receive initial data. */ 40 | public record Idle() implements ParserState { 41 | 42 | @Override 43 | public ParserState parse(byte[] data) { 44 | var accumulating = new Accumulating(ByteBuffer.allocate(256), -1); 45 | 46 | return accumulating.parse(data); 47 | } 48 | } 49 | 50 | public record Accumulating(ByteBuffer buffer, int expectedLength) implements ParserState { 51 | 52 | @Override 53 | public ParserState parse(byte[] data) { 54 | buffer.put(data); 55 | 56 | int readableBytes = buffer.position(); 57 | 58 | if (readableBytes < 3 || readableBytes < expectedLength) { 59 | return this; 60 | } 61 | 62 | byte fcb = buffer.get(1); 63 | 64 | switch (fcb & 0xFF) { 65 | case 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 -> { 66 | int fixedLength = 1 + (1 + 2 + 2) + 2; 67 | if (readableBytes >= fixedLength) { 68 | ModbusRtuFrame frame = readFrame(buffer.flip(), fixedLength); 69 | return new Accumulated(frame); 70 | } else { 71 | return new Accumulating(buffer, fixedLength); 72 | } 73 | } 74 | 75 | case 0x0F, 0x10 -> { 76 | int minimum = 1 + (1 + 2 + 2 + 1) + 2; 77 | if (readableBytes >= minimum) { 78 | int byteCount = buffer.get(6); 79 | if (readableBytes >= minimum + byteCount) { 80 | ModbusRtuFrame frame = readFrame(buffer.flip(), minimum + byteCount); 81 | return new Accumulated(frame); 82 | } else { 83 | return new Accumulating(buffer, minimum + byteCount); 84 | } 85 | } else { 86 | return new Accumulating(buffer, minimum); 87 | } 88 | } 89 | 90 | case 0x16 -> { 91 | int fixedLength = 1 + (1 + 2 + 2 + 2) + 2; 92 | if (readableBytes >= fixedLength) { 93 | ModbusRtuFrame frame = readFrame(buffer.flip(), fixedLength); 94 | return new Accumulated(frame); 95 | } else { 96 | return new Accumulating(buffer, fixedLength); 97 | } 98 | } 99 | 100 | case 0x17 -> { 101 | int minimum = 1 + (1 + 2 + 2 + 2 + 2 + 1) + 2; 102 | if (readableBytes >= minimum) { 103 | int byteCount = buffer.get(10); 104 | if (readableBytes >= minimum + byteCount) { 105 | ModbusRtuFrame frame = readFrame(buffer.flip(), minimum + byteCount); 106 | return new Accumulated(frame); 107 | } else { 108 | return new Accumulating(buffer, minimum + byteCount); 109 | } 110 | } else { 111 | return new Accumulating(buffer, minimum); 112 | } 113 | } 114 | 115 | default -> { 116 | return new ParseError(buffer, "unsupported function code: 0x%02X".formatted(fcb)); 117 | } 118 | } 119 | } 120 | 121 | private static ModbusRtuFrame readFrame(ByteBuffer buffer, int length) { 122 | int slaveId = buffer.get() & 0xFF; 123 | 124 | ByteBuffer payload = buffer.slice(buffer.position(), length - 3); 125 | ByteBuffer crc = buffer.slice(buffer.position() + length - 3, 2); 126 | 127 | return new ModbusRtuFrame(slaveId, payload, crc); 128 | } 129 | } 130 | 131 | public record Accumulated(ModbusRtuFrame frame) implements ParserState { 132 | 133 | @Override 134 | public ParserState parse(byte[] data) { 135 | return this; 136 | } 137 | } 138 | 139 | public record ParseError(ByteBuffer buffer, String message) implements ParserState { 140 | 141 | @Override 142 | public ParserState parse(byte[] data) { 143 | buffer.put(data); 144 | return this; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /modbus-tests/src/test/java/com/digitalpetri/modbus/test/ModbusTcpClientServerIT.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.test; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertTrue; 4 | 5 | import com.digitalpetri.modbus.ModbusPduSerializer.DefaultRequestSerializer; 6 | import com.digitalpetri.modbus.client.ModbusClient; 7 | import com.digitalpetri.modbus.client.ModbusTcpClient; 8 | import com.digitalpetri.modbus.internal.util.Hex; 9 | import com.digitalpetri.modbus.pdu.ReadHoldingRegistersRequest; 10 | import com.digitalpetri.modbus.server.ModbusServer; 11 | import com.digitalpetri.modbus.server.ModbusTcpServer; 12 | import com.digitalpetri.modbus.server.ProcessImage; 13 | import com.digitalpetri.modbus.server.ReadWriteModbusServices; 14 | import com.digitalpetri.modbus.tcp.Netty; 15 | import com.digitalpetri.modbus.tcp.client.NettyTcpClientTransport; 16 | import com.digitalpetri.modbus.tcp.client.NettyTcpClientTransport.ConnectionListener; 17 | import com.digitalpetri.modbus.tcp.client.NettyTimeoutScheduler; 18 | import com.digitalpetri.modbus.tcp.server.NettyTcpServerTransport; 19 | import java.nio.ByteBuffer; 20 | import java.util.Optional; 21 | import java.util.concurrent.CountDownLatch; 22 | import java.util.concurrent.TimeUnit; 23 | import org.junit.jupiter.api.AfterEach; 24 | import org.junit.jupiter.api.BeforeEach; 25 | import org.junit.jupiter.api.Test; 26 | 27 | public class ModbusTcpClientServerIT extends ClientServerIT { 28 | 29 | ModbusTcpClient client; 30 | ModbusTcpServer server; 31 | 32 | NettyTcpClientTransport clientTransport; 33 | 34 | @BeforeEach 35 | void setup() throws Exception { 36 | var processImage = new ProcessImage(); 37 | var modbusServices = 38 | new ReadWriteModbusServices() { 39 | @Override 40 | protected Optional getProcessImage(int unitId) { 41 | return Optional.of(processImage); 42 | } 43 | }; 44 | 45 | int serverPort = -1; 46 | 47 | for (int i = 50200; i < 65536; i++) { 48 | try { 49 | final var port = i; 50 | var serverTransport = 51 | NettyTcpServerTransport.create( 52 | cfg -> { 53 | cfg.bindAddress = "localhost"; 54 | cfg.port = port; 55 | }); 56 | 57 | System.out.println("trying port " + port); 58 | server = ModbusTcpServer.create(serverTransport, modbusServices); 59 | server.start(); 60 | serverPort = port; 61 | break; 62 | } catch (Exception e) { 63 | server = null; 64 | } 65 | } 66 | 67 | if (server == null) { 68 | throw new Exception("Failed to start server"); 69 | } 70 | 71 | final var port = serverPort; 72 | clientTransport = 73 | NettyTcpClientTransport.create( 74 | cfg -> { 75 | cfg.hostname = "localhost"; 76 | cfg.port = port; 77 | cfg.connectPersistent = false; 78 | }); 79 | 80 | client = 81 | ModbusTcpClient.create( 82 | clientTransport, 83 | cfg -> cfg.timeoutScheduler = new NettyTimeoutScheduler(Netty.sharedWheelTimer())); 84 | client.connect(); 85 | } 86 | 87 | @AfterEach 88 | void teardown() throws Exception { 89 | if (client != null) { 90 | client.disconnect(); 91 | } 92 | if (server != null) { 93 | server.stop(); 94 | } 95 | } 96 | 97 | @Override 98 | ModbusClient getClient() { 99 | return client; 100 | } 101 | 102 | @Override 103 | ModbusServer getServer() { 104 | return server; 105 | } 106 | 107 | @Test 108 | void sendRaw() throws Exception { 109 | var request = new ReadHoldingRegistersRequest(0, 10); 110 | ByteBuffer buffer = ByteBuffer.allocate(256); 111 | DefaultRequestSerializer.INSTANCE.encode(request, buffer); 112 | 113 | byte[] requestedPduBytes = new byte[buffer.position()]; 114 | buffer.flip(); 115 | buffer.get(requestedPduBytes); 116 | 117 | System.out.println("requestedPduBytes: " + Hex.format(requestedPduBytes)); 118 | 119 | byte[] responsePduBytes = client.sendRaw(0, requestedPduBytes); 120 | 121 | System.out.println("responsePduBytes: " + Hex.format(responsePduBytes)); 122 | } 123 | 124 | @Test 125 | void connectionListener() throws Exception { 126 | var onConnection = new CountDownLatch(1); 127 | var onConnectionLost = new CountDownLatch(1); 128 | 129 | clientTransport.addConnectionListener( 130 | new ConnectionListener() { 131 | @Override 132 | public void onConnection() { 133 | onConnection.countDown(); 134 | } 135 | 136 | @Override 137 | public void onConnectionLost() { 138 | onConnectionLost.countDown(); 139 | } 140 | }); 141 | 142 | assertTrue(client.isConnected()); 143 | 144 | client.disconnect(); 145 | assertTrue(onConnectionLost.await(1, TimeUnit.SECONDS)); 146 | 147 | client.connect(); 148 | assertTrue(onConnection.await(1, TimeUnit.SECONDS)); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/authz/ReadWriteAuthzHandler.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server.authz; 2 | 3 | import com.digitalpetri.modbus.pdu.MaskWriteRegisterRequest; 4 | import com.digitalpetri.modbus.pdu.ReadCoilsRequest; 5 | import com.digitalpetri.modbus.pdu.ReadDiscreteInputsRequest; 6 | import com.digitalpetri.modbus.pdu.ReadHoldingRegistersRequest; 7 | import com.digitalpetri.modbus.pdu.ReadInputRegistersRequest; 8 | import com.digitalpetri.modbus.pdu.ReadWriteMultipleRegistersRequest; 9 | import com.digitalpetri.modbus.pdu.WriteMultipleCoilsRequest; 10 | import com.digitalpetri.modbus.pdu.WriteMultipleRegistersRequest; 11 | import com.digitalpetri.modbus.pdu.WriteSingleCoilRequest; 12 | import com.digitalpetri.modbus.pdu.WriteSingleRegisterRequest; 13 | 14 | /** 15 | * A simplified {@link AuthzHandler} that determines authorization based on whether the client has 16 | * read or write access to a given unit id. 17 | * 18 | *

Subclasses must implement {@link #authorizeRead(int, AuthzContext)} and {@link 19 | * #authorizeWrite(int, AuthzContext)}. The default implementations of the read and write methods 20 | * will call these methods to determine authorization. 21 | * 22 | *

Operations that require read authorization: 23 | * 24 | *

    25 | *
  • ReadCoils 26 | *
  • ReadDiscreteInputs 27 | *
  • ReadHoldingRegisters 28 | *
  • ReadInputRegisters 29 | *
30 | * 31 | *

Operations that require write authorization: 32 | * 33 | *

    34 | *
  • WriteSingleCoil 35 | *
  • WriteSingleRegister 36 | *
  • WriteMultipleCoils 37 | *
  • WriteMultipleRegisters 38 | *
  • MaskWriteRegister 39 | *
40 | * 41 | *

Operations that require both read and write authorization: 42 | * 43 | *

    44 | *
  • ReadWriteMultipleRegisters 45 | *
46 | */ 47 | public abstract class ReadWriteAuthzHandler implements AuthzHandler { 48 | 49 | @Override 50 | public AuthzResult authorizeReadCoils( 51 | AuthzContext authzContext, int unitId, ReadCoilsRequest request) { 52 | 53 | return authorizeRead(unitId, authzContext); 54 | } 55 | 56 | @Override 57 | public AuthzResult authorizeReadDiscreteInputs( 58 | AuthzContext authzContext, int unitId, ReadDiscreteInputsRequest request) { 59 | 60 | return authorizeRead(unitId, authzContext); 61 | } 62 | 63 | @Override 64 | public AuthzResult authorizeReadHoldingRegisters( 65 | AuthzContext authzContext, int unitId, ReadHoldingRegistersRequest request) { 66 | 67 | return authorizeRead(unitId, authzContext); 68 | } 69 | 70 | @Override 71 | public AuthzResult authorizeReadInputRegisters( 72 | AuthzContext authzContext, int unitId, ReadInputRegistersRequest request) { 73 | 74 | return authorizeRead(unitId, authzContext); 75 | } 76 | 77 | @Override 78 | public AuthzResult authorizeWriteSingleCoil( 79 | AuthzContext authzContext, int unitId, WriteSingleCoilRequest request) { 80 | 81 | return authorizeWrite(unitId, authzContext); 82 | } 83 | 84 | @Override 85 | public AuthzResult authorizeWriteSingleRegister( 86 | AuthzContext authzContext, int unitId, WriteSingleRegisterRequest request) { 87 | 88 | return authorizeWrite(unitId, authzContext); 89 | } 90 | 91 | @Override 92 | public AuthzResult authorizeWriteMultipleCoils( 93 | AuthzContext authzContext, int unitId, WriteMultipleCoilsRequest request) { 94 | 95 | return authorizeWrite(unitId, authzContext); 96 | } 97 | 98 | @Override 99 | public AuthzResult authorizeWriteMultipleRegisters( 100 | AuthzContext authzContext, int unitId, WriteMultipleRegistersRequest request) { 101 | 102 | return authorizeWrite(unitId, authzContext); 103 | } 104 | 105 | @Override 106 | public AuthzResult authorizeMaskWriteRegister( 107 | AuthzContext authzContext, int unitId, MaskWriteRegisterRequest request) { 108 | 109 | return authorizeWrite(unitId, authzContext); 110 | } 111 | 112 | @Override 113 | public AuthzResult authorizeReadWriteMultipleRegisters( 114 | AuthzContext authzContext, int unitId, ReadWriteMultipleRegistersRequest request) { 115 | 116 | AuthzResult readResult = authorizeRead(unitId, authzContext); 117 | AuthzResult writeResult = authorizeWrite(unitId, authzContext); 118 | 119 | if (readResult == AuthzResult.AUTHORIZED && writeResult == AuthzResult.AUTHORIZED) { 120 | return AuthzResult.AUTHORIZED; 121 | } else { 122 | return AuthzResult.NOT_AUTHORIZED; 123 | } 124 | } 125 | 126 | /** 127 | * Authorize a read operation against the given unit id. 128 | * 129 | * @param unitId the unit id to authorize against. 130 | * @param authzContext the {@link AuthzContext}. 131 | * @return the result of the authorization check. 132 | */ 133 | protected abstract AuthzResult authorizeRead(int unitId, AuthzContext authzContext); 134 | 135 | /** 136 | * Authorize a write operation against the given unit id. 137 | * 138 | * @param unitId the unit id to authorize against. 139 | * @param authzContext the {@link AuthzContext}. 140 | * @return the result of the authorization check. 141 | */ 142 | protected abstract AuthzResult authorizeWrite(int unitId, AuthzContext authzContext); 143 | } 144 | -------------------------------------------------------------------------------- /modbus-tests/src/test/java/com/digitalpetri/modbus/test/ClientServerIT.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.test; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import com.digitalpetri.modbus.client.ModbusClient; 8 | import com.digitalpetri.modbus.exceptions.ModbusExecutionException; 9 | import com.digitalpetri.modbus.pdu.MaskWriteRegisterRequest; 10 | import com.digitalpetri.modbus.pdu.MaskWriteRegisterResponse; 11 | import com.digitalpetri.modbus.pdu.ReadCoilsRequest; 12 | import com.digitalpetri.modbus.pdu.ReadCoilsResponse; 13 | import com.digitalpetri.modbus.pdu.ReadDiscreteInputsRequest; 14 | import com.digitalpetri.modbus.pdu.ReadDiscreteInputsResponse; 15 | import com.digitalpetri.modbus.pdu.ReadHoldingRegistersRequest; 16 | import com.digitalpetri.modbus.pdu.ReadHoldingRegistersResponse; 17 | import com.digitalpetri.modbus.pdu.ReadInputRegistersRequest; 18 | import com.digitalpetri.modbus.pdu.ReadInputRegistersResponse; 19 | import com.digitalpetri.modbus.pdu.ReadWriteMultipleRegistersRequest; 20 | import com.digitalpetri.modbus.pdu.ReadWriteMultipleRegistersResponse; 21 | import com.digitalpetri.modbus.pdu.WriteMultipleCoilsRequest; 22 | import com.digitalpetri.modbus.pdu.WriteMultipleCoilsResponse; 23 | import com.digitalpetri.modbus.pdu.WriteMultipleRegistersRequest; 24 | import com.digitalpetri.modbus.pdu.WriteMultipleRegistersResponse; 25 | import com.digitalpetri.modbus.pdu.WriteSingleCoilRequest; 26 | import com.digitalpetri.modbus.pdu.WriteSingleCoilResponse; 27 | import com.digitalpetri.modbus.pdu.WriteSingleRegisterRequest; 28 | import com.digitalpetri.modbus.pdu.WriteSingleRegisterResponse; 29 | import com.digitalpetri.modbus.server.ModbusServer; 30 | import org.junit.jupiter.api.Test; 31 | 32 | public abstract class ClientServerIT { 33 | 34 | abstract ModbusClient getClient(); 35 | 36 | abstract ModbusServer getServer(); 37 | 38 | @Test 39 | void readCoils() throws Exception { 40 | ReadCoilsResponse response = getClient().readCoils(1, new ReadCoilsRequest(0, 2000)); 41 | 42 | assertEquals(250, response.coils().length); 43 | } 44 | 45 | @Test 46 | void readDiscreteInputs() throws Exception { 47 | ReadDiscreteInputsResponse response = 48 | getClient().readDiscreteInputs(1, new ReadDiscreteInputsRequest(0, 2000)); 49 | 50 | assertEquals(250, response.inputs().length); 51 | } 52 | 53 | @Test 54 | void readHoldingRegisters() throws Exception { 55 | ReadHoldingRegistersResponse response = 56 | getClient().readHoldingRegisters(1, new ReadHoldingRegistersRequest(0, 125)); 57 | 58 | assertEquals(250, response.registers().length); 59 | } 60 | 61 | @Test 62 | void readInputRegisters() throws Exception { 63 | ReadInputRegistersResponse response = 64 | getClient().readInputRegisters(1, new ReadInputRegistersRequest(0, 125)); 65 | 66 | assertEquals(250, response.registers().length); 67 | } 68 | 69 | @Test 70 | void writeSingleCoil() throws Exception { 71 | WriteSingleCoilResponse response = 72 | getClient().writeSingleCoil(1, new WriteSingleCoilRequest(0, true)); 73 | 74 | assertEquals(0, response.address()); 75 | assertEquals(0xFF00, response.value()); 76 | } 77 | 78 | @Test 79 | void writeSingleRegister() throws Exception { 80 | WriteSingleRegisterResponse response = 81 | getClient().writeSingleRegister(1, new WriteSingleRegisterRequest(0, 0x1234)); 82 | 83 | assertEquals(0, response.address()); 84 | assertEquals(0x1234, response.value()); 85 | } 86 | 87 | @Test 88 | void writeMultipleCoils() throws Exception { 89 | WriteMultipleCoilsResponse response = 90 | getClient() 91 | .writeMultipleCoils(1, new WriteMultipleCoilsRequest(0, 8, new byte[] {(byte) 0xFF})); 92 | 93 | assertEquals(0, response.address()); 94 | assertEquals(8, response.quantity()); 95 | } 96 | 97 | @Test 98 | void writeMultipleRegisters() throws Exception { 99 | WriteMultipleRegistersResponse response = 100 | getClient() 101 | .writeMultipleRegisters( 102 | 1, new WriteMultipleRegistersRequest(0, 1, new byte[] {0x12, 0x34})); 103 | 104 | assertEquals(0, response.address()); 105 | assertEquals(1, response.quantity()); 106 | } 107 | 108 | @Test 109 | void maskWriteRegister() throws Exception { 110 | MaskWriteRegisterResponse response = 111 | getClient().maskWriteRegister(1, new MaskWriteRegisterRequest(0, 0xFF00, 0x00FF)); 112 | 113 | assertEquals(0, response.address()); 114 | assertEquals(0xFF00, response.andMask()); 115 | assertEquals(0x00FF, response.orMask()); 116 | } 117 | 118 | @Test 119 | void readWriteMultipleRegisters() throws Exception { 120 | ReadWriteMultipleRegistersResponse response = 121 | getClient() 122 | .readWriteMultipleRegisters( 123 | 1, new ReadWriteMultipleRegistersRequest(0, 1, 0, 1, new byte[] {0x12, 0x34})); 124 | 125 | byte[] registers = response.registers(); 126 | assertEquals(0x12, registers[0]); 127 | assertEquals(0x34, registers[1]); 128 | } 129 | 130 | @Test 131 | void isConnected() throws ModbusExecutionException { 132 | assertTrue(getClient().isConnected()); 133 | 134 | getClient().disconnect(); 135 | 136 | assertFalse(getClient().isConnected()); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/server/authz/AuthzHandler.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus.server.authz; 2 | 3 | import com.digitalpetri.modbus.pdu.MaskWriteRegisterRequest; 4 | import com.digitalpetri.modbus.pdu.ReadCoilsRequest; 5 | import com.digitalpetri.modbus.pdu.ReadDiscreteInputsRequest; 6 | import com.digitalpetri.modbus.pdu.ReadHoldingRegistersRequest; 7 | import com.digitalpetri.modbus.pdu.ReadInputRegistersRequest; 8 | import com.digitalpetri.modbus.pdu.ReadWriteMultipleRegistersRequest; 9 | import com.digitalpetri.modbus.pdu.WriteMultipleCoilsRequest; 10 | import com.digitalpetri.modbus.pdu.WriteMultipleRegistersRequest; 11 | import com.digitalpetri.modbus.pdu.WriteSingleCoilRequest; 12 | import com.digitalpetri.modbus.pdu.WriteSingleRegisterRequest; 13 | 14 | /** Callback interface that handles authorization of Modbus operations. */ 15 | public interface AuthzHandler { 16 | 17 | /** 18 | * Authorizes the reading of coils. 19 | * 20 | * @param authzContext the {@link AuthzContext}. 21 | * @param unitId the unit identifier of the Modbus device. 22 | * @param request the {@link ReadCoilsRequest} being authorized. 23 | * @return the result of the authorization check. 24 | */ 25 | AuthzResult authorizeReadCoils(AuthzContext authzContext, int unitId, ReadCoilsRequest request); 26 | 27 | /** 28 | * Authorizes the reading of discrete inputs. 29 | * 30 | * @param authzContext the {@link AuthzContext}. 31 | * @param unitId the unit identifier of the Modbus device. 32 | * @param request the {@link ReadDiscreteInputsRequest} being authorized. 33 | * @return the result of the authorization check. 34 | */ 35 | AuthzResult authorizeReadDiscreteInputs( 36 | AuthzContext authzContext, int unitId, ReadDiscreteInputsRequest request); 37 | 38 | /** 39 | * Authorizes the reading of holding registers. 40 | * 41 | * @param authzContext the {@link AuthzContext}. 42 | * @param unitId the unit identifier of the Modbus device. 43 | * @param request the {@link ReadHoldingRegistersRequest} being authorized. 44 | * @return the result of the authorization check. 45 | */ 46 | AuthzResult authorizeReadHoldingRegisters( 47 | AuthzContext authzContext, int unitId, ReadHoldingRegistersRequest request); 48 | 49 | /** 50 | * Authorizes the reading of input registers. 51 | * 52 | * @param authzContext the {@link AuthzContext}. 53 | * @param unitId the unit identifier of the Modbus device. 54 | * @param request the {@link ReadInputRegistersRequest} being authorized. 55 | * @return the result of the authorization check. 56 | */ 57 | AuthzResult authorizeReadInputRegisters( 58 | AuthzContext authzContext, int unitId, ReadInputRegistersRequest request); 59 | 60 | /** 61 | * Authorizes the writing of a single coil. 62 | * 63 | * @param authzContext the {@link AuthzContext}. 64 | * @param unitId the unit identifier of the Modbus device. 65 | * @param request the {@link WriteSingleCoilRequest} being authorized. 66 | * @return the result of the authorization check. 67 | */ 68 | AuthzResult authorizeWriteSingleCoil( 69 | AuthzContext authzContext, int unitId, WriteSingleCoilRequest request); 70 | 71 | /** 72 | * Authorizes the writing of a single register. 73 | * 74 | * @param authzContext the {@link AuthzContext}. 75 | * @param unitId the unit identifier of the Modbus device. 76 | * @param request the {@link WriteSingleRegisterRequest} being authorized. 77 | * @return the result of the authorization check. 78 | */ 79 | AuthzResult authorizeWriteSingleRegister( 80 | AuthzContext authzContext, int unitId, WriteSingleRegisterRequest request); 81 | 82 | /** 83 | * Authorizes the writing of multiple coils. 84 | * 85 | * @param authzContext the {@link AuthzContext}. 86 | * @param unitId the unit identifier of the Modbus device. 87 | * @param request the {@link WriteMultipleCoilsRequest} being authorized. 88 | * @return the result of the authorization check. 89 | */ 90 | AuthzResult authorizeWriteMultipleCoils( 91 | AuthzContext authzContext, int unitId, WriteMultipleCoilsRequest request); 92 | 93 | /** 94 | * Authorizes the writing of multiple registers. 95 | * 96 | * @param authzContext the {@link AuthzContext}. 97 | * @param unitId the unit identifier of the Modbus device. 98 | * @param request the {@link WriteMultipleRegistersRequest} being authorized. 99 | * @return the result of the authorization check. 100 | */ 101 | AuthzResult authorizeWriteMultipleRegisters( 102 | AuthzContext authzContext, int unitId, WriteMultipleRegistersRequest request); 103 | 104 | /** 105 | * Authorizes the mask write register operation. 106 | * 107 | * @param authzContext the {@link AuthzContext}. 108 | * @param unitId the unit identifier of the Modbus device. 109 | * @param request the {@link MaskWriteRegisterRequest} being authorized. 110 | * @return the result of the authorization check. 111 | */ 112 | AuthzResult authorizeMaskWriteRegister( 113 | AuthzContext authzContext, int unitId, MaskWriteRegisterRequest request); 114 | 115 | /** 116 | * Authorizes the read-write-multiple registers operation. 117 | * 118 | * @param authzContext the {@link AuthzContext}. 119 | * @param unitId the unit identifier of the Modbus device. 120 | * @param request the {@link ReadWriteMultipleRegistersRequest} being authorized. 121 | * @return the result of the authorization check. 122 | */ 123 | AuthzResult authorizeReadWriteMultipleRegisters( 124 | AuthzContext authzContext, int unitId, ReadWriteMultipleRegistersRequest request); 125 | 126 | /** Enumeration representing the result of an authorization check. */ 127 | enum AuthzResult { 128 | 129 | /** Indicates that the operation is authorized. */ 130 | AUTHORIZED, 131 | 132 | /** Indicates that the operation is not authorized. */ 133 | NOT_AUTHORIZED 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/ModbusRtuResponseFrameParser.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.modbus; 2 | 3 | import com.digitalpetri.modbus.internal.util.Hex; 4 | import java.nio.ByteBuffer; 5 | import java.util.StringJoiner; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | 8 | public class ModbusRtuResponseFrameParser { 9 | 10 | private final AtomicReference state = new AtomicReference<>(new Idle()); 11 | 12 | /** 13 | * Parse incoming data and return the updated {@link ParserState}. 14 | * 15 | * @param data the incoming data to parse. 16 | * @return the updated {@link ParserState}. 17 | */ 18 | public ParserState parse(byte[] data) { 19 | return state.updateAndGet(s -> s.parse(data)); 20 | } 21 | 22 | /** 23 | * Get the current {@link ParserState}. 24 | * 25 | * @return the current {@link ParserState}. 26 | */ 27 | public ParserState getState() { 28 | return state.get(); 29 | } 30 | 31 | /** Reset this parser to the {@link Idle} state. */ 32 | public ParserState reset() { 33 | return state.getAndSet(new Idle()); 34 | } 35 | 36 | public sealed interface ParserState permits Idle, Accumulating, Accumulated, ParseError { 37 | 38 | ParserState parse(byte[] data); 39 | } 40 | 41 | /** Waiting to receive initial data. */ 42 | public record Idle() implements ParserState { 43 | 44 | @Override 45 | public ParserState parse(byte[] data) { 46 | var accumulating = new Accumulating(ByteBuffer.allocate(256), -1); 47 | 48 | return accumulating.parse(data); 49 | } 50 | } 51 | 52 | /** 53 | * Holds a {@link ByteBuffer} that accumulates data and, if known, the expected total length. 54 | * 55 | * @param buffer the buffer to accumulate data in. 56 | * @param expectedLength the expected total length; -1 if not yet known. 57 | */ 58 | public record Accumulating(ByteBuffer buffer, int expectedLength) implements ParserState { 59 | 60 | @Override 61 | public ParserState parse(byte[] data) { 62 | buffer.put(data); 63 | 64 | int readableBytes = buffer.position(); 65 | 66 | if (readableBytes < 3 || readableBytes < expectedLength) { 67 | return this; 68 | } 69 | 70 | byte fcb = buffer.get(1); 71 | 72 | switch (fcb & 0xFF) { 73 | case 0x01, 0x02, 0x03, 0x04, 0x17 -> { 74 | int count = buffer.get(2) & 0xFF; 75 | int calculatedLength = count + 5; 76 | 77 | if (readableBytes >= calculatedLength) { 78 | ModbusRtuFrame frame = readFrame(buffer.flip(), calculatedLength); 79 | return new Accumulated(frame); 80 | } else { 81 | return new Accumulating(buffer, calculatedLength); 82 | } 83 | } 84 | case 0x05, 0x06, 0x0F, 0x10 -> { 85 | // the body of each of these is 4 bytes, so we know the total length 86 | int fixedLength = 8; 87 | if (readableBytes >= fixedLength) { 88 | ModbusRtuFrame frame = readFrame(buffer.flip(), fixedLength); 89 | return new Accumulated(frame); 90 | } else { 91 | return new Accumulating(buffer, fixedLength); 92 | } 93 | } 94 | case 0x16 -> { 95 | int fixedLength = 10; 96 | if (readableBytes >= fixedLength) { 97 | ModbusRtuFrame frame = readFrame(buffer.flip(), fixedLength); 98 | return new Accumulated(frame); 99 | } else { 100 | return new Accumulating(buffer, fixedLength); 101 | } 102 | } 103 | case 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x8F, 0x90, 0x96 -> { 104 | // error response for one of the supported function codes 105 | int fixedLength = 5; 106 | if (readableBytes >= fixedLength) { 107 | ModbusRtuFrame frame = readFrame(buffer.flip(), fixedLength); 108 | return new Accumulated(frame); 109 | } else { 110 | return new Accumulating(buffer, fixedLength); 111 | } 112 | } 113 | default -> { 114 | return new ParseError(buffer, "unsupported function code: 0x%02X".formatted(fcb)); 115 | } 116 | } 117 | } 118 | 119 | private static ModbusRtuFrame readFrame(ByteBuffer buffer, int length) { 120 | int slaveId = buffer.get() & 0xFF; 121 | 122 | ByteBuffer payload = buffer.slice(buffer.position(), length - 3); 123 | ByteBuffer crc = buffer.slice(buffer.position() + length - 3, 2); 124 | 125 | return new ModbusRtuFrame(slaveId, payload, crc); 126 | } 127 | 128 | @Override 129 | public String toString() { 130 | int limit = buffer.limit(); 131 | int position = buffer.position(); 132 | buffer.flip(); 133 | try { 134 | return new StringJoiner(", ", Accumulating.class.getSimpleName() + "[", "]") 135 | .add("buffer=" + Hex.format(buffer)) 136 | .add("expectedLength=" + expectedLength) 137 | .toString(); 138 | } finally { 139 | buffer.limit(limit).position(position); 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * Contains an accumulated {@link ModbusRtuFrame}, with a PDU of some function code we understand 146 | * enough to parse. The CRC has not been validated. 147 | * 148 | * @param frame the accumulated {@link ModbusRtuFrame}. 149 | */ 150 | public record Accumulated(ModbusRtuFrame frame) implements ParserState { 151 | 152 | @Override 153 | public ParserState parse(byte[] data) { 154 | return this; 155 | } 156 | } 157 | 158 | /** 159 | * Parser received a function code it doesn't recognize. 160 | * 161 | * @param buffer the accumulated data buffer. 162 | * @param error a message describing the error. 163 | */ 164 | public record ParseError(ByteBuffer buffer, String error) implements ParserState { 165 | 166 | @Override 167 | public ParserState parse(byte[] data) { 168 | buffer.put(data); 169 | return this; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /modbus/src/main/java/com/digitalpetri/modbus/Modbus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Kevin Herron 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.digitalpetri.modbus; 18 | 19 | import java.util.concurrent.ExecutorService; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.ScheduledExecutorService; 22 | import java.util.concurrent.ScheduledThreadPoolExecutor; 23 | import java.util.concurrent.ThreadFactory; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.concurrent.atomic.AtomicLong; 26 | import org.slf4j.LoggerFactory; 27 | 28 | /** 29 | * Shared resources that, if not otherwise provided, can be used as defaults. 30 | * 31 | *

These resources should be released when the JVM is shutting down or the ClassLoader that 32 | * loaded this library is unloaded. 33 | * 34 | *

See {@link #releaseSharedResources()}. 35 | */ 36 | public final class Modbus { 37 | 38 | private Modbus() {} 39 | 40 | private static ExecutorService EXECUTOR_SERVICE; 41 | private static ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE; 42 | 43 | /** 44 | * @return a shared {@link ExecutorService}. 45 | */ 46 | public static synchronized ExecutorService sharedExecutor() { 47 | if (EXECUTOR_SERVICE == null) { 48 | ThreadFactory threadFactory = 49 | new ThreadFactory() { 50 | private final AtomicLong threadNumber = new AtomicLong(0L); 51 | 52 | @Override 53 | public Thread newThread(Runnable r) { 54 | Thread thread = 55 | new Thread(r, "modbus-shared-thread-pool-" + threadNumber.getAndIncrement()); 56 | thread.setDaemon(true); 57 | thread.setUncaughtExceptionHandler( 58 | (t, e) -> 59 | LoggerFactory.getLogger(Modbus.class) 60 | .warn("Uncaught Exception on shared stack ExecutorService thread", e)); 61 | return thread; 62 | } 63 | }; 64 | 65 | EXECUTOR_SERVICE = Executors.newCachedThreadPool(threadFactory); 66 | } 67 | 68 | return EXECUTOR_SERVICE; 69 | } 70 | 71 | /** 72 | * @return a shared {@link ScheduledExecutorService}. 73 | */ 74 | public static synchronized ScheduledExecutorService sharedScheduledExecutor() { 75 | if (SCHEDULED_EXECUTOR_SERVICE == null) { 76 | ThreadFactory threadFactory = 77 | new ThreadFactory() { 78 | private final AtomicLong threadNumber = new AtomicLong(0L); 79 | 80 | @Override 81 | public Thread newThread(Runnable r) { 82 | Thread thread = 83 | new Thread( 84 | r, "modbus-shared-scheduled-executor-" + threadNumber.getAndIncrement()); 85 | thread.setDaemon(true); 86 | thread.setUncaughtExceptionHandler( 87 | (t, e) -> 88 | LoggerFactory.getLogger(Modbus.class) 89 | .warn( 90 | "Uncaught Exception on shared stack ScheduledExecutorService thread", 91 | e)); 92 | return thread; 93 | } 94 | }; 95 | 96 | var executor = new ScheduledThreadPoolExecutor(1, threadFactory); 97 | 98 | executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); 99 | 100 | SCHEDULED_EXECUTOR_SERVICE = executor; 101 | } 102 | 103 | return SCHEDULED_EXECUTOR_SERVICE; 104 | } 105 | 106 | /** 107 | * Release shared resources, waiting at most 5 seconds for each of the shared resources to shut 108 | * down gracefully. 109 | * 110 | * @see #releaseSharedResources(long, TimeUnit) 111 | */ 112 | public static synchronized void releaseSharedResources() { 113 | releaseSharedResources(5, TimeUnit.SECONDS); 114 | } 115 | 116 | /** 117 | * Release shared resources, waiting at most the specified timeout for each of the shared 118 | * resources to shut down gracefully. 119 | * 120 | * @param timeout the duration of the timeout. 121 | * @param unit the unit of the timeout duration. 122 | */ 123 | public static synchronized void releaseSharedResources(long timeout, TimeUnit unit) { 124 | if (EXECUTOR_SERVICE != null) { 125 | EXECUTOR_SERVICE.shutdown(); 126 | } 127 | 128 | if (SCHEDULED_EXECUTOR_SERVICE != null) { 129 | SCHEDULED_EXECUTOR_SERVICE.shutdown(); 130 | } 131 | 132 | if (EXECUTOR_SERVICE != null) { 133 | try { 134 | if (!EXECUTOR_SERVICE.awaitTermination(timeout, unit)) { 135 | LoggerFactory.getLogger(Modbus.class) 136 | .warn("ExecutorService not shut down after {} {}.", timeout, unit); 137 | } 138 | } catch (InterruptedException e) { 139 | Thread.currentThread().interrupt(); 140 | LoggerFactory.getLogger(Modbus.class) 141 | .warn("Interrupted awaiting executor service shutdown", e); 142 | } 143 | EXECUTOR_SERVICE = null; 144 | } 145 | 146 | if (SCHEDULED_EXECUTOR_SERVICE != null) { 147 | try { 148 | if (!SCHEDULED_EXECUTOR_SERVICE.awaitTermination(timeout, unit)) { 149 | LoggerFactory.getLogger(Modbus.class) 150 | .warn("ScheduledExecutorService not shut down after {} {}.", timeout, unit); 151 | } 152 | } catch (InterruptedException e) { 153 | Thread.currentThread().interrupt(); 154 | LoggerFactory.getLogger(Modbus.class) 155 | .warn("Interrupted awaiting scheduled executor service shutdown", e); 156 | } 157 | SCHEDULED_EXECUTOR_SERVICE = null; 158 | } 159 | } 160 | } 161 | --------------------------------------------------------------------------------