Skip to content
Kevin Herron edited this page Oct 20, 2021 · 4 revisions

Client SDK

Connecting

The entry point for access to a remote server is via an instance of OpcUaClient. An instance can be created by providing an OpcUaClientConfig to the static OpcUaClient#create() function.

Creating an OpcUaClientConfig requires, at a minimum, a valid EndpointDescription for the server this client will connect to. An EndpointDescription can be obtained by calling the GetEndpoints service and selecting an endpoint that matches the criteria of the client application.

Each EndpointDescription contains details about an endpoint such as the endpoint URL, SecurityPolicy, MessageSecurityMode, and UserTokenPolicys supported by that endpoint.

Putting this together, the process looks like:

  1. Given an endpoint URL, call the GetEndpoints service to get a list of endpoints.
  2. Select an endpoint that meets your application criteria.
  3. Create and configure an OpcUaClientConfig using the selected endpoint.
  4. Create an OpcUaClient instance and call connect().

OpcUaClient#create has an overload that combines the above steps into a single call. The following example creates an OpcUaClient after selecting the first endpoint that uses SecurityPolicy.None.

OpcUaClient client = OpcUaClient.create(
    "opc.tcp://milo.digitalpetri.com:62541/milo",
    endpoints ->
        endpoints.stream()
            .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
            .findFirst(),
    configBuilder ->
        configBuilder.build()
);

Now the OpcUaClient just needs to be connected:

client.connect().get();

From this call forward the OpcUaClient instance will automatically attempt to reconnect any time the connection is lost, including if the initial connect() call failed. This behavior persists until disconnect() is called.

Discovery

Endpoints can be discovered by calling the GetEndpoints service on a remote server.

The client SDK provides a DiscoveryClient with convenient static helper methods to make this easier.

A list of EndpointDescriptions can be obtained from DiscoveryClient#getEndpoints:

List<EndpointDescription> endpoints =
    DiscoveryClient.getEndpoints(endpointUrl).get();

Configuration

The above example showed an extremely minimal configuriation for a client, but properly configured client applications need a little more configuration to identify them.

  • Application Name
  • Application URI
  • Product URI

Security

In order to connect to an endpoint that uses security additional configuration is needed.

  • Public+Private Key Pair
  • Certificate or Certificate Chain
  • Certificate Validator

Authentication

Authentication is configured by setting an IdentityProvider when building an OpcUaClientConfig. You must configure an IdentityProvider that the selected endpoint indicates it supports in its UserTokenPolicy array.

Anonymous

No additional configuration is necessary to connect anonymously as AnonymousProvider is the default IdentityProvider when one is not explicitly configured.

The selected endpoint must contain a UserTokenPolicy with a UserTokenType of UserTokenType.Anonymous.

Username and Password

To connect with a username and password set an instance of UsernameProvider when building the OpcUaClientConfig.

The selected endpoint must contain a UserTokenPolicy with a UserTokenType of UserTokenType.UserName.

X509 Certificate

To connect using an X509Certificate as the identity set an instance of X509IdentityProvider

The selected endpoint must contain a UserTokenPolicy with a UserTokenType of UserTokenType.Certificate.

Browsing

Browsing can be done in three ways:

  1. AddressSpace#browse, a higher level API that provides blocking or non-blocking options and a reasonable set of default parameters.

    Using the default BrowseOptions:

    AddressSpace addressSpace = client.getAddressSpace();
    
    UaNode serverNode = addressSpace.getNode(Identifiers.Server);
    
    List<? extends UaNode> nodes = addressSpace.browseNodes(serverNode);

    Using a custom BrowseOptions:

    AddressSpace addressSpace = client.getAddressSpace();
    
    UaNode serverNode = addressSpace.getNode(Identifiers.Server);
    
    BrowseOptions browseOptions = addressSpace.getBrowseOptions().copy(
      b ->
        b.setReferenceType(BuiltinReferenceType.HasProperty)
    );
    
    // just UaNodes referenced by HasProperty references
    List<? extends UaNode> nodes = addressSpace.browseNodes(serverNode, browseOptions);
  2. UaNode#browse and UaNode#browseNodes, a higher level API that makes getting References or other UaNode instances referenced by a UaNode instance easy.

    BrowseOptions browseOptions = BrowseOptions.builder()
        .setReferenceType(Identifiers.HasProperty)
        .build();
    
    // Browse for HasProperty references
    List<ReferenceDescription> references = node.browse(browseOptions);
    
    // Browse for HasProperty references and get the UaNodes they target
    List<? extends UaNode> nodes = node.browseNodes(browseOptions);
  3. OpcUaClient#browse, a lower level API that takes parameters corresponding directly to those defined for the Browse service in the OPC UA spec.

     // Browse for forward hierarchal references from the Objects folder
     // that lead to other Object and Variable nodes.
     BrowseDescription browse = new BrowseDescription(
         Identifiers.ObjectsFolder,
         BrowseDirection.Forward,
         Identifiers.References,
         true,
         uint(NodeClass.Object.getValue() | NodeClass.Variable.getValue()),
         uint(BrowseResultMask.All.getValue())
     );
    
     BrowseResult browseResult = client.browse(browse).get();

    Unlike the previous browse methods, this one does not check whether a continuation point was returned and follow up with calls to the BrowseNext service until all References have been retrieved. Subsequent calls to BrowseNext are the responsibility of the caller.

Reading

Attributes can be read using either the high or low level APIs:

  1. Using UaNode, which provides a variety of high level calls to read each attribute directly or provide an AttributeId to read.

    UaVariableNode testNode = (UaVariableNode) addressSpace.getNode(
        new NodeId(2, "TestInt32")
    );
    
    // Read the Value attribute
    DataValue value = testNode.readValue();
    
    // Read the BrowseName attribute
    QualifiedName browseName = testNode.readBrowseName();
    
    // Read the Description attribute, with timestamps and quality intact
    DataValue descriptionValue = testNode.readAttribute(AttributeId.Description);
  2. Using OpcUaClient#read, a lower level API that takes parameters corresponding directly to those defined for the Read service in the OPC UA spec.

    List<ReadValueId> readValueIds = new ArrayList<>();
    
    readValueIds.add(
        new ReadValueId(
            new NodeId(2, "TestInt32"),
            AttributeId.Value.uid(),
            null, // indexRange
            QualifiedName.NULL_VALUE
        )
    );
    
    ReadResponse readResponse = client.read(
      0.0, // maxAge
      TimestampsToReturn.Both, 
      readValueIds
    ).get();

Writing

Attributes can be written using either the high or low level APIs:

  1. Using UaNode, which provides a variety of high level calls to write each attribute directly or provide an AttributeId and DataValue to write.

    UaVariableNode testNode = (UaVariableNode) addressSpace.getNode(
        new NodeId(2, "TestInt32")
    );
    
    // Write the Value attribute; throws UaException if the write fails
    testNode.writeValue(new Variant(42));
    
    // Most servers don't allow quality or timestamps to be written,
    // hence the use of DataValue.valueOnly(), but this method could
    // be used to write to a server that did support it if necessary
    StatusCode statusCode = testNode.writeAttribute(
      AttributeId.Value, 
      DataValue.valueOnly(new Variant(42))
    );
  2. Using OpcUaClient#write, a lower level API that takes parameters corresponding directly to those defined for the Write service in the OPC UA spec.

    List<WriteValue> writeValues = new ArrayList<>();
    
    writeValues.add(
        new WriteValue(
            new NodeId(2, "TestInt32"),
            AttributeId.Value.uid(),
            null, // indexRange
            DataValue.valueOnly(new Variant(42))
        )
    );
    
    WriteResponse writeResponse = client.write(writeValues).get();

Methods

Methods can be called using either the high or low level APIs:

  1. Using UaObjectNode#callMethod:

     UaObjectNode serverNode = addressSpace.getObjectNode(Identifiers.Server);
    
     Variant[] outputs = serverNode.callMethod(
         "GetMonitoredItems",
         new Variant[]{
             new Variant(subscription.getSubscription().getSubscriptionId())
         }
     );
  2. Finding a UaMethod and invoking UaMethod#call:

    UaObjectNode serverNode = addressSpace.getObjectNode(Identifiers.Server);
    
    UaMethod getMonitoredItems = serverNode.getMethod("GetMonitoredItems");
    
    Variant[] outputs = getMonitoredItems.call(
        new Variant[]{
            new Variant(subscription.getSubscription().getSubscriptionId())
        }
    );

    The UaMethod object also has a copy of the input and output defined by the method:

    Argument[] inputArguments = getMonitoredItems.getInputArguments();
    Argument[] outputArguments = getMonitoredItems.getOutputArguments();

    When a method does not define input or output arguments the corresponding Argument[] will be empty.

  3. OpcUaClient#call, a lower level API That takes parameters corresponding directly to those defined by the Call service in the OPC UA spec:

    NodeId objectId = NodeId.parse("ns=2;s=HelloWorld");
    NodeId methodId = NodeId.parse("ns=2;s=HelloWorld/sqrt(x)");
    
    CallMethodRequest request = new CallMethodRequest(
        objectId,
        methodId,
        new Variant[]{new Variant(input)}
    );
    
    return client.call(request).thenCompose(result -> {
        StatusCode statusCode = result.getStatusCode();
    
        if (statusCode.isGood()) {
            Double value = (Double) l(result.getOutputArguments()).get(0).getValue();
            return CompletableFuture.completedFuture(value);
        } else {
            StatusCode[] inputArgumentResults = result.getInputArgumentResults();
            for (int i = 0; i < inputArgumentResults.length; i++) {
                logger.error("inputArgumentResults[{}]={}", i, inputArgumentResults[i]);
            }
            
            CompletableFuture<Double> f = new CompletableFuture<>();
            f.completeExceptionally(new UaException(statusCode));
            return f;
        }
    });

    See MethodExample.java in the client-examples Maven module for the full source code.

Subscriptions

ManagedSubscription

ManagedSubscription is the entry point in the higher level API for creating Subscriptions and MonitoredItems.

Create a ManagedSubscription using an OpcUaClient instance like this:

ManagedSubscription subscription = ManagedSubscription.create(client);

Optionally, specify a publishing interval when creating the subscription:

ManagedSubscription subscription = ManagedSubscription.create(client, 250.0);

Once the Subscription is created you can add a data or event listener to it. This listener will receive all data changes for all items belonging to the subscription.

subscription.addChangeListener(new ChangeListener() {
    @Override
    public void onDataReceived(List<ManagedDataItem> dataItems, List<DataValue> dataValues) {

        // Each item in the dataItems list has a corresponding value at 
        // the same index in the dataValues list.
        // Some items may appear multiple times if the item has a queue 
        // size greater than 1 and the value changed more than once within 
        // the publishing interval of the subscription. 
        // The items and values appear in the order of the changes.
    }
});

ManagedDataItem

A ManagedDataItem represents a MonitoredItem monitoring an attribute value (as opposed to an Event).

Create a ManagedDataItem using a ManagedSubscription instance like this:

ManagedDataItem dataItem = subscription.createDataItem(Identifiers.Server_ServerStatus_CurrentTime);

if (!dataItem.getStatusCode.isGood()) {
    throw new RuntimeException("uh oh!")
}

Always check the StatusCode after creating or modifying a ManagedDataItem.

ManagedEventItem

A ManagedEventItem represents a MonitoredItem that will receive Events from an Objects EventNotifier attribute.

When subscribing to an Event a filter is required that specifies the types of events and fields that will be reported. Create a ManagedEventItem using a ManagedSubscription instance like this:

EventFilter eventFilter = new EventFilter(
    new SimpleAttributeOperand[]{
        new SimpleAttributeOperand(
            Identifiers.BaseEventType,
            new QualifiedName[]{new QualifiedName(0, "EventId")},
            AttributeId.Value.uid(),
            null),
        new SimpleAttributeOperand(
            Identifiers.BaseEventType,
            new QualifiedName[]{new QualifiedName(0, "Time")},
            AttributeId.Value.uid(),
            null),
        new SimpleAttributeOperand(
            Identifiers.BaseEventType,
            new QualifiedName[]{new QualifiedName(0, "Message")},
            AttributeId.Value.uid(),
            null)
    },
    new ContentFilter(null)
);

// Subscribe for Events from the Server Node's EventNotifier attribute.
ManagedEventItem eventItem = subscription.createEventItem(Identifiers.Server, eventFilter);

Events are reported to any ChangeListeners registered on the ManagedSubscription:

subscription.addChangeListener(new ChangeListener() {
    @Override
    public void onEventReceived(List<ManagedEventItem> eventItems, List<Variant[]> eventFields) {
        
        // Each item in the eventItems list has a corresponding set of 
        // event field values at the same index in the eventFields list.
        // The number of fields and their meaning depend on the filter.
    }
});

Because ManagedEventItems can be created with different filters, it may be easier to register an EventValueListener directly on each event item instead:

eventItem.addEventValueListener(new ManagedEventItem.EventValueListener() {
    @Override
    public void onEventValueReceived(ManagedEventItem item, Variant[] value) {
        
        // A new event arrived, do something with it.
    }
});

OpcUaSubscriptionManager

OpcUaSubscriptionManager is the entry point into the lower level API for creating Subscriptions and MonitoredItems.

OpcUaSubscriptionManager#createSubscription is used to create a UaSubscription object. MonitoredItems can then be created using UaSubscription#createMonitoredItems. The parameters and data structures used in these API calls correspond directly to those defined by the Subscription and MonitoredItems services in the OPC UA spec.

See SubscriptionExample.java in the client-examples Maven module for an example.

UaNode

Instances of UaNode can be obtained from the AddressSpace. When these instances are created for the first time all of the Node's attributes are read.

Attributes

Every UaNode contains a local copy of those attributes that can be accessed without causing a round-trip communication to the server. Use the get<attribute>() methods to access these values.

Fresh attribute values can be obtained using read<attribute>() methods and written using the write<attribute>() methods. When a read or a write succeeds it also updates the local attribute value.

Local attribute values can be set using the set<attribute>() methods, which affects only the local value. See #synchronize() to write these values to the server.

Refresh

UaNode#refresh(Set<AttributeId>) will do a bulk read for the identified set of attributes and update the local attribute values if it succeeds.

Synchronize

UaNode#synchronize(Set<AttributeId>) will do a bulk write for the identified set of attributes using the local attribute values.