Building a Modbus TCP Slave Server in Java with Value Change Listeners
Establishing a Modbus TCP slave endpoint in Java commonly leverages dedicated protocol libraries. The following implementation demonstrates how to configure the environment, bootstrap the network listener, populate the holding register memory map, and attach an observer for real-time mutation tracking.
Dependency Resolution
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.3</version>
</dependency>
Configuration Note: Certain corporate proxies or aggregated repository mirrors occasionally route malformed requests for this artifact. If dependency resolution fails, switch explicitly to Maven Central or verify your local firewall/DNS forwarding rules.
Endpoint Initialization
import com.serotonin.modbus4j.ModbusFactory;
import com.serotonin.modbus4j.ModbusSlaveSet;
import com.serotonin.modbus4j.ip.tcp.TcpSlave;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class ModbusEndpointStarter implements ApplicationRunner {
@Override
public void run(ApplicationArguments arguments) throws Exception {
// Launch the protocol listener on a detached worker thread to preserve application context loading
Thread networkThread = new Thread(this::bindModbusDaemon, "Modbus-IoT-Worker");
networkThread.setDaemon(true);
networkThread.start();
// Allow sufficient latency for socket binding and descriptor allocation
Thread.sleep(1200);
// Trigger secondary registration or cache-seeding routines here
// populateStaticMappings();
}
private void bindModbusDaemon() {
ModbusFactory protocolGateway = new ModbusFactory();
// Instantiate TCP listener on standard industrial port 502
ModbusSlaveSet deviceEndpoint = new TcpSlave(502, false);
deviceEndpoint.addProcessImage(MemoryLayoutMapper.generateDefaultMap());
try {
System.out.println("Modbus TCP node bound and operational.");
deviceEndpoint.start();
} catch (Exception transportError) {
transportError.printStackTrace();
}
}
}
Memory Layout & Register Seeding
import com.serotonin.modbus4j.BasicProcessImage;
public class MemoryLayoutMapper {
private static final int HOLDING_SPACE_LIMIT = 608;
public static BasicProcessImage generateDefaultMap() {
// Reserve addressing range for logical device ID 1
BasicProcessImage storageRegion = new BasicProcessImage(1);
// Pre-allocate holding registers with zeroed states
for (int pointer = 0; pointer < HOLDING_SPACE_LIMIT; pointer++) {
storageRegion.setHoldingRegister(pointer, (short) 0x0000);
}
// Register the modification watcher against the process image
storageRegion.addListener(new HoldingRangeWatcher());
return storageRegion;
}
}
Mutation Event Subscriber
import com.serotonin.modbus4j.ProcessImageListener;
public class HoldingRangeWatcher implements ProcessImageListener {
@Override
public void coilWrite(int targetAddress, boolean initialStatus, boolean updatedStatus) {
// Coil-level events are excluded from this monitoring scope
}
@Override
public void holdingRegisterWrite(int registerOffset, short previousValue, short newValue) {
System.out.printf("Holding register #%d transitioned from %d to %d%n",
registerOffset, previousValue, newValue);
}
}
Upon deployment, the application binds to port 502 and continuously accepts incoming telegrams. Validation can be performed using standard diagnostic utilities like Modbus Poll or QModMaster. Establishing a session successfully indicates that the underlying socket is active and the exposed process image is responding to queries.