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:

ab-eth

ads

canopen

eip

firmata

genericcan

iec-60870-5-104

knxnet-ip

logix

modbus-ascii

modbus-rtu

modbus-tcp

opcua

plc4x

s7

simulated

slmp

umas

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. simulated reports 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

getKey()

String

The parameter name as it appears in a connection string.

getType()

OptionType

Pick the right widget and validate input. See below.

isRequired()

boolean

Mark the field mandatory and block submission while it is empty.

getDefaultValue()

Optional<Object>

Pre-fill the field. Empty means there is no default.

getDescription()

String

Tooltip or help text.

isSecret()

boolean

Render as a password field and keep it out of logs.

getSince()

Optional<String>

PLC4X version that introduced the option, e.g. 0.13.0. Only some options carry it.

OptionType is a small enum - BOOLEAN, INT, LONG, FLOAT, DOUBLE, STRING, FILE, STRUCT:

  • FILE is a path - offer a file picker (used for keystores and certificates).

  • STRUCT is a composite value the driver parses from its string form, such as the ADS target-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 - ConnectionBase reports an operation as supported exactly if the connection class implements the corresponding hook. ctrlx, for instance, reports all four as false, 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. s7 is 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 reports false for 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 unordered Set - sort before displaying.

  • Filter the test transport out of anything a user sees.

  • A driver may report no transports (simulated) and no options (simulated again). Every metadata accessor that can be absent returns an Optional or an empty list - none of them return null, 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: PlcDriverMetadata describes what a driver can be configured with and needs no device, PlcConnectionMetadata describes what an established connection supports and exists only after a successful connect.