Getting Started with Java

Using the PLC4J API directly

In order to write a valid PLC4X Java application, all you need, is to add a dependency to the api module. When using Maven, all you need to do is add this dependency:

    <dependency>
      <groupId>org.apache.plc4x</groupId>
      <artifactId>plc4j-api</artifactId>
      <version>0.12.0</version>
    </dependency>

This will allow you to write a valid application, that compiles fine. However, in order to actually connect to a device using a given protocol, you need to add this protocol implementation to the classpath.

For example in order to communicate with an S7 device using the S7 Protocol, you would need to add the following dependency:

    <dependency>
      <groupId>org.apache.plc4x</groupId>
      <artifactId>plc4j-driver-s7</artifactId>
      <version>0.12.0</version>
      <scope>runtime</scope>
    </dependency>

So as soon as your project has the API and a driver implementation available, you first need to get a PlcConnection instance. This is done via the PlcConnectionManager, which is provided to you by the PlcDriverManager by asking this to create an instance for a given PLC4X connection string.

String connectionString = "s7://10.10.64.20";

try (PlcConnection plcConnection = PlcDriverManager.getDefault().getConnectionManager().getConnection(connectionString)) {

  ... do something with the connection here ...

}

PLC4X generally supports a very limited set of functions, which is not due to the fact, that we didn’t implement things, but that PLCs generally support a very limited set of functions.

The basic functions supported by PLCs and therefore supported by PLC4X are:

  • Discover Devices

  • List resources in the PLC

  • Read data

  • Write data

  • Subscribe for data

In general, we will try to offer as many features as possible. So if a protocol doesn’t support subscription based communication it is our goal to simulate this by polling in the background, so it is transparent for the users (This simulation feature hasn’t been implemented yet though, but it’s on our roadmap).

But there are some cases in which we can’t simulate or features are simply disabled intentionally:

  • If a PLC and/or protocol don’t support writing or browsing, we simply can’t provide this functionality.

  • Also do we plan on providing stripped down versions of drivers, that for example intentionally don’t support any writing of data.

Therefore, we use metadata to check programmatically, if a given feature is available.

Reading Data

// Check if this connection support reading of data.
if (!plcConnection.getMetadata().isReadSupported()) {
  logger.error("This connection doesn't support reading.");
  return;
}

As soon as you have ensured that a feature is available, you are ready to build a first request. This is done by getting a PlcReadRequest.Builder:

// Create a new read request:
// - Give the single item requested an alias name
PlcReadRequest.Builder builder = plcConnection.readRequestBuilder();
builder.addTagAddress("value-1", "%Q0.4:BOOL");
builder.addTagAddress("value-2", "%Q0:BYTE");
builder.addTagAddress("value-3", "%I0.2:BOOL");
builder.addTagAddress("value-4", "%DB.DB1.4:INT");
PlcReadRequest readRequest = builder.build();

So, as you can see, you prepare a request, by adding tag addresses to the request and in the end by calling the build method.

If you are using the BrowseApi you might also have been provided with Tag objects. In that case simply use addTag and pass in the Tag object instead of the address string.

The request is sent to the PLC by issuing the execute method on the request object:

CompletableFuture<? extends PlcReadResponse> asyncResponse = readRequest.execute();
asyncResponse.whenComplete((response, throwable) -> {
  try {
    ... process the response ...
  } catch (Exception e) {
    ... Handle any errors ...
  }
});

In general, all requests are executed asynchronously. As soon as the request is fully processed, the callback gets called and will contain a readResponse, if everything went right or a throwable if there were problems.

However, if you want to write your code in a more synchronous fashion, the following alternative will provide this:

PlcReadResponse response = readRequest.execute().get(5000, TimeUnit.MILLISECONDS);

Processing of the responses is identical in both cases in the synchronous approach you however need to catch any exceptions.

The following example will demonstrate some of the options you have:

for (String tagName : response.getTagNames()) {
    if(response.getResponseCode(tagName) == PlcResponseCode.OK) {
        int numValues = response.getNumberOfValues(tagName);
        // If it's just one element, output just one single line.
        if(numValues == 1) {
            logger.info("Value[" + tagName + "]: " + response.getObject(tagName));
        }
        // If it's more than one element, output each in a single row.
        else {
            logger.info("Value[" + tagName + "]:");
            for(int i = 0; i < numValues; i++) {
                logger.info(" - " + response.getObject(tagName, i));
            }
        }
    }
    // Something went wrong, to output an error message instead.
    else {
        logger.error("Error[" + tagName + "]: " + response.getResponseCode(tagName).name());
    }
}

In the for-loop, we are demonstrating how the user can iterate over the tag aliases in the response. In case of an ordinary read request, this will be predefined by the items in the request, however in case of a subscription response, the response might only contain some of the items that were subscribed.

Before accessing the data, it is advisable to check if an item was correctly returned. This is done by the getResponseCode method for a given alias. If this is PlcResponseCode.OK, everything is ok, however it could be one of the following:

  • NOT_FOUND

  • ACCESS_DENIED

  • INVALID_ADDRESS

  • INVALID_DATATYPE

  • INTERNAL_ERROR

  • RESPONSE_PENDING

Assuming the return code was OK, we can continue accessing the data.

As some tags support reading arrays, with the method getNumberOfValues the user can check how many items of a given type are returned. For convenience the response object has single-argument methods for accessing the data, which default to returning the first element.

response.getObject(fieldName)

If you want to access a given element number, please use the two-argument version instead:

response.getObject(fieldName, 42)

PLC4X provides getters and setters for a wide variety of Java types and automatically handles the type conversion. However, when for example trying to get a long-value as a byte and the long-value exceeds the range supported by the smaller type, a RuntimeException of type PlcIncompatibleDatatypeException. In order to avoid causing this exception to be thrown, however there are isValid{TypeName} methods that you can use to check if the value is compatible.

Writing Data

In general the structure of code for writing data is extremely similar to that of reading data.

So first it is advisable to check if this connection is even able to write data:

// Check if this connection support writing of data.
if (!plcConnection.getMetadata().isWriteSupported()) {
  logger.error("This connection doesn't support writing.");
  return;
}

As soon as we are sure that we can write, we create a new PlcWriteRequest.Builder:

// Create a new write request:
// - Give the single item requested an alias name
// - Pass in the data you want to write (for arrays, pass in one value for every element)
PlcWriteRequest.Builder builder = plcConnection.writeRequestBuilder();
builder.addTagAddress("value-1", "%Q0.4:BOOL", new PlcBOOL(true));
builder.addTagAddress("value-2", "%Q0:BYTE", new PlcBYTE((byte) 0xFF));
builder.addTagAddress("value-4", "%DB.DB1.4:INT[3]", new PlcINT(7), new PlcINT(23), new PlcINT(42));
PlcWriteRequest writeRequest = builder.build();

The same way read requests are sent to the PLC by issuing the execute method on the request object:

CompletableFuture<? extends PlcWriteResponse> asyncResponse = writeRequest.execute();
asyncResponse.whenComplete((response, throwable) -> {
  ... process the response ...
});

You could here also use the blocking option:

PlcWriteResponse response = writeRequest.execute().get();

As we don’t have to process the data itself, for the write request, it’s enough to simply check the return code for each field.

for (String tagName : response.getTagNames()) {
    if(response.getResponseCode(tagName) == PlcResponseCode.OK) {
        logger.info("Value[" + tagName + "]: updated");
    }
    // Something went wrong, to output an error message instead.
    else {
        logger.error("Error[" + tagName + "]: " + response.getResponseCode(tagName).name());
    }
}

Subscribing to Data

Subscribing to data can be considered similar to reading data, at least the subscription itself if very similar to reading of data.

First of all we first have to check if the connection supports this:

// Check if this connection support subscribing to data.
if (!plcConnection.getMetadata().isSubscribeSupported()) {
    logger.error("This connection doesn't support subscribing.");
    return;
}

Now we’ll create the subscription request.

The main difference is that while reading there is only one form how you could read, with subscriptions there are different forms of subscriptions:

  • Change of state (Event is sent as soon as a value changes)

  • Cyclic (The Event is sent in regular cyclic intervals)

  • Event (The Event is usually explicitly sent form the PLC as a signal)

Therefore instead of using a normal addItem or addTag in newer versions, there are tree different methods as you can see in the following examples.

// Create a new subscription request:
// - Give the single tag requested an alias name
PlcSubscriptionRequest.Builder builder = plcConnection.subscriptionRequestBuilder();
builder.addChangeOfStateTagAddress("value-1", "{some address}");
builder.addCyclicTagAddress("value-2", "{some address}", Duration.ofMillis(1000));
builder.addEventTagAddress("value-3", "{some alarm address}");
PlcSubscriptionRequest subscriptionRequest = builder.build();
Note
The addCyclicField/addCyclicTagAddress method requires a third parameter duration.

The request itself is executed exactly the same way the read and write operations are executed, using the execute method, therefore just the short synchronous version here (The async version works just as good)

PlcSubscriptionResponse response = subscriptionRequest.execute().get();

Now comes the little more tricky part, as subscriptions are always asynchronous, we have to register a callback for the connection to call as soon as there is news available:

In general, you can’t say how many of your subscribed fields will be available in every callback. So it is double important to check or iterate over the field names.

for (String subscriptionName : response.getFieldNames()) {
    final PlcSubscriptionHandle subscriptionHandle = response.getSubscriptionHandle(subscriptionName);
    subscriptionHandle.register(plcSubscriptionEvent -> {
        for (String tagName : plcSubscriptionEvent.getTagNames()) {
            System.out.println(plcSubscriptionEvent.getPlcValue(tagName));
        }
    });
}
Note
Here there currently is a double iteration over the field names, this will probably change soon.