Discovering Drivers and their Configuration
This page is for people integrating PLC4X into another tool: a connection dialog, a config form, a validation step in a pipeline, a node in a flow-based editor.
You do not have to hard-code any of it. Every driver ships a description of itself, and PLC4X can hand you that description at runtime: which drivers are on the classpath, which transports each one speaks, and every configuration parameter each combination accepts - with its type, whether it is mandatory, and its default.
This is the same metadata the protocol pages on this site are generated from, so what your tool shows and what the documentation says can never drift apart.
This API is part of PLC4J (Java). PLC4Go, PLC4Py and PLC4C do not expose an equivalent
yet - PLC4Go’s GetMetadata() describes a connection, not a driver.
|
Where it starts
Everything hangs off PlcDriverManager:
import org.apache.plc4x.java.api.PlcDriverManager;
PlcDriverManager driverManager = PlcDriverManager.getDefault();
Drivers are found with Java’s ServiceLoader, so "which drivers do I have" literally means
"which driver jars are on the classpath". If your tool loads plugins into their own classloader,
hand it over explicitly:
import org.apache.plc4x.java.DefaultPlcDriverManager;
PlcDriverManager driverManager = new DefaultPlcDriverManager(myPluginClassLoader);
Which drivers do I have?
Set<String> protocolCodes = driverManager.getProtocolCodes(); (1)
PlcDriver driver = driverManager.getDriver("s7"); (2)
String humanReadable = driver.getProtocolName(); (3)
| 1 | The short codes - the s7 in s7://10.0.0.1. |
| 2 | Throws PlcConnectionException if no driver with that code is on the classpath. |
| 3 | A display name, e.g. Siemens S7 (Basic). Use the code for wiring, the name for humans. |
getProtocolCodes() returns an unordered Set. Sort it before showing it to anyone,
or your dropdown will shuffle itself between runs.
|
If you already have a connection string and just want the driver behind it, use
driverManager.getDriverForUrl(url) instead of parsing the scheme yourself.
With the plc4j-driver-all dependency, the above yields 18 drivers:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Which transports does a driver support?
Everything else lives on PlcDriverMetadata:
import org.apache.plc4x.java.api.metadata.PlcDriverMetadata;
PlcDriverMetadata metadata = driver.getMetadata();
List<String> transports = metadata.getSupportedTransportCodes(); (1)
Optional<String> preferred = metadata.getDefaultTransportCode(); (2)
boolean canDiscover = metadata.isDiscoverySupported(); (3)
| 1 | e.g. [tcp, tls, tls-psk, udp, test] for modbus-tcp. |
| 2 | What the driver uses when the connection string names no transport. Pre-select this. |
| 3 | Whether the driver can search the network for devices - use it to enable a "Scan" button. |
Two things to handle:
-
Filter out
test. It is an in-memory transport used by PLC4X’s own unit tests. It shows up in the list, but it is not something a user can pick. The protocol pages on this site filter it out for exactly this reason. -
The list can be empty.
simulatedreports no transports at all, because it never talks to anything. Do not assume at least one.
List<String> selectable = metadata.getSupportedTransportCodes().stream()
.filter(code -> !"test".equals(code))
.toList();
What is the configuration for a driver?
import org.apache.plc4x.java.api.metadata.OptionMetadata;
Optional<OptionMetadata> protocolOptions =
metadata.getProtocolConfigurationOptionMetadata();
It is an Optional because a driver may declare no options of its own - simulated has none.
What is the configuration for a transport of that driver?
Ask for the pair, never for the transport alone:
Optional<OptionMetadata> transportOptions =
metadata.getTransportConfigurationOptionMetadata("tcp");
A driver’s transport configuration is exposed per driver-and-transport combination, so always query it through the driver you are configuring rather than caching one table per transport code.
What parameters does a configuration define?
OptionMetadata gives you the list, and Option describes each entry:
import org.apache.plc4x.java.api.metadata.Option;
List<Option> all = protocolOptions.get().getOptions();
List<Option> mandatory = protocolOptions.get().getRequiredOptions(); // convenience filter
| Method | Returns | What to do with it |
|---|---|---|
|
|
The parameter name as it appears in a connection string. |
|
|
Pick the right widget and validate input. See below. |
|
|
Mark the field mandatory and block submission while it is empty. |
|
|
Pre-fill the field. Empty means there is no default. |
|
|
Tooltip or help text. |
|
|
Render as a password field and keep it out of logs. |
|
|
PLC4X version that introduced the option, e.g. |
OptionType is a small enum - BOOLEAN, INT, LONG, FLOAT, DOUBLE, STRING, FILE,
STRUCT:
-
FILEis a path - offer a file picker (used for keystores and certificates). -
STRUCTis a composite value the driver parses from its string form, such as the ADStarget-ams-net-id. Treat it as free text and let the driver validate it; the syntax is documented on the driver’s own protocol page.
Most options are optional and defaulted. ads is the one driver in the default set that really
demands input - four mandatory parameters, two of them STRUCT:
target-ams-net-id STRUCT required
target-ams-port INT required
source-ams-net-id STRUCT required
source-ams-port INT required
Turning options back into a connection string
The keys are used verbatim as query parameters, with one rule: transport options are namespaced with their transport code, protocol options are not.
s7://10.0.0.1?pdu-size=2048&cotp.local-rack=1&cotp.remote-slot=2
^ ^ ^
| | └── transport option, prefixed with "cotp."
| └── transport option
└── protocol option, no prefix
So when you build the string, prefix exactly the values that came from
getTransportConfigurationOptionMetadata(…):
String key = (transportCode == null) ? option.getKey()
: transportCode + "." + option.getKey();
Anything marked isSecret() ends up in that same string. PLC4X redacts those parameters in
its own log output - do the same before you log, display or persist a connection string.
|
A complete example
This program prints the full catalogue - every driver, its transports, and every option with its type, whether it is mandatory, and its default:
import org.apache.plc4x.java.api.PlcDriver;
import org.apache.plc4x.java.api.PlcDriverManager;
import org.apache.plc4x.java.api.metadata.Option;
import org.apache.plc4x.java.api.metadata.OptionMetadata;
import org.apache.plc4x.java.api.metadata.PlcDriverMetadata;
import java.util.List;
import java.util.TreeSet;
public class DriverCatalog {
public static void main(String[] args) throws Exception {
PlcDriverManager driverManager = PlcDriverManager.getDefault();
for (String protocolCode : new TreeSet<>(driverManager.getProtocolCodes())) {
PlcDriver driver = driverManager.getDriver(protocolCode);
PlcDriverMetadata metadata = driver.getMetadata();
System.out.println("== " + protocolCode + " (" + driver.getProtocolName() + ")");
System.out.println(" discovery supported : " + metadata.isDiscoverySupported());
System.out.println(" default transport : "
+ metadata.getDefaultTransportCode().orElse("<none>"));
// "test" is an in-memory transport used by PLC4X's own unit tests.
List<String> transports = metadata.getSupportedTransportCodes().stream()
.filter(code -> !"test".equals(code))
.toList();
System.out.println(" transports : " + transports);
metadata.getProtocolConfigurationOptionMetadata()
.ifPresent(options -> print(" protocol options", options, null));
for (String transportCode : transports) {
metadata.getTransportConfigurationOptionMetadata(transportCode)
.ifPresent(options ->
print(" transport options (" + transportCode + ")", options, transportCode));
}
System.out.println();
}
}
private static void print(String heading, OptionMetadata metadata, String prefix) {
System.out.println(heading + ":");
for (Option option : metadata.getOptions()) {
// In a connection string, transport options are namespaced with the transport code.
String key = (prefix == null) ? option.getKey() : prefix + "." + option.getKey();
System.out.printf(" %-34s %-8s %-9s %-7s %s%n",
key,
option.getType(),
option.isRequired() ? "required" : "optional",
option.isSecret() ? "secret" : "",
option.getDefaultValue().map(v -> "default=" + v).orElse(""));
}
}
}
Its output for the ADS driver:
== ads (Beckhoff TwinCat ADS)
discovery supported : true
default transport : tcp
transports : [tcp]
protocol options:
target-ams-net-id STRUCT required
target-ams-port INT required
source-ams-net-id STRUCT required
source-ams-port INT required
request-timeout-ms INT optional default=4000
max-data-type-table-depth INT optional default=20
load-symbol-and-data-type-tables BOOLEAN optional default=true
transport options (tcp):
tcp.connect-timeout-ms INT optional default=5000
tcp.read-timeout-ms INT optional default=0
tcp.write-timeout-ms INT optional default=0
tcp.no-delay BOOLEAN optional default=true
tcp.keep-alive BOOLEAN optional default=false
tcp.send-buffer-size INT optional default=81920
tcp.receive-buffer-size INT optional default=81920
tcp.local-address STRING optional
tcp.local-port INT optional default=0
To run it, you need a driver on the classpath. plc4j-driver-all gives you every one of them:
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-driver-all</artifactId>
<version>1.1.0</version>
</dependency>
What can a connection actually do?
Everything above describes a driver and needs no device, which is what makes it useful while a user is still filling in a connection dialog. Which operations you may actually call is a different question, and it can only be answered once you are connected:
import org.apache.plc4x.java.api.PlcConnection;
import org.apache.plc4x.java.api.metadata.PlcConnectionMetadata;
try (PlcConnection connection =
driverManager.getConnectionFactory().getConnection("s7://10.0.0.1")) {
PlcConnectionMetadata metadata = connection.getMetadata();
boolean canRead = metadata.isReadSupported(); (1)
boolean canWrite = metadata.isWriteSupported();
boolean canSubscribe = metadata.isSubscribeSupported();
boolean canBrowse = metadata.isBrowseSupported();
}
| 1 | Four booleans, one per operation. Gate your buttons, menu entries and pipeline steps on them. |
getConnection(…) connects before it returns, so by the time you hold a PlcConnection the
answer is available. There is no earlier moment to ask: PlcConnectionMetadata hangs off the
connection, not off the driver, and a connection that never connected cannot tell you what the
device at the other end offers.
That split is the whole point, because "unsupported" has two quite different causes:
-
The driver does not implement the operation. This is a property of the driver, and the default answer is derived from it -
ConnectionBasereports an operation as supported exactly if the connection class implements the corresponding hook.ctrlx, for instance, reports all four asfalse, because its read, write and subscribe builders are not implemented and its browse implementation does not work yet. -
The device does not offer it. Only the driver can know this, and only after talking to the device.
s7is the example: reading and writing are always supported, while browsing and subscribing ride S7Comm UserData services, so the driver probes for them during connect and reportsfalsefor a device - a LOGO, typically - that does not answer. The same driver and the same connection string therefore give different answers for different controllers, which is precisely why this cannot be a static property of the driver.
Check the metadata before building a request, not after. A driver that does not support
an operation is not obliged to fail politely - ctrlx returns null from its request builders,
so an unchecked call gets you a NullPointerException rather than a helpful exception.
|
If you hand out connections through the connection cache, the
leased connection delegates getMetadata() to the connection it borrowed, so you get the same
answers. Ask while you hold the lease: once it is returned, every method on it - this one
included - throws a PlcRuntimeException.
PLC4Go has the same concept under different names: connection.GetMetadata() returns a
PlcConnectionMetadata whose methods are called CanRead(), CanWrite(), CanSubscribe() and
CanBrowse(), plus a GetConnectionAttributes() map. It is the one piece of metadata that is
not Java-only - what PLC4Go has no equivalent for is everything else on this page.
|
Things to watch out for
-
getProtocolCodes()is an unorderedSet- sort before displaying. -
Filter the
testtransport out of anything a user sees. -
A driver may report no transports (
simulated) and no options (simulatedagain). Every metadata accessor that can be absent returns anOptionalor an empty list - none of them returnnull, but none of them promise content either. -
Ask for transport options per driver-and-transport pair, not per transport code.
-
Treat
isSecret()options as credentials everywhere: masked in the UI, redacted in logs. -
Driver metadata and connection metadata are different things:
PlcDriverMetadatadescribes what a driver can be configured with and needs no device,PlcConnectionMetadatadescribes what an established connection supports and exists only after a successful connect.