1. Introduction
This is the Prosys OPC UA SDK for Java tutorial for client application development. With this quick introduction you should be able to understand the basic ideas behind the Prosys OPC UA SDK for Java.
Note that this tutorial assumes that you are already familiar with the basic concepts of OPC UA communications, although you can get started without much prior knowledge.
For a start on OPC UA communications, we recommend the book OPC Unified Architecture by Mahnke, Leitner and Damm (Springer-Verlag, 2009, ISBN 978-3-540-68898-3). For a full reference, you can use the OPC UA specification.
2. Installation
See the installation instructions in the 'README.txt' file (or the brief version on the download page). The README file also contains notes about the usage and deployment of external libraries used by the SDK.
There is also a basic starting guide with tips on Java development tools and on using the Prosys OPC UA SDK for Java with the Eclipse IDE located in the 'Prosys_OPC_UA_SDK_for_Java_Starting_Guide' next to this tutorial in the distribution package.
3. Sample Applications
The SDK contains a sample client application in the SampleConsoleClient
Java class. This tutorial will refer to the code in the sample application while explaining the different steps to take in order to accomplish the main tasks of an OPC UA client.
The samples also contain a SimpleClient
Java class that presents a very straightforward example of the most simplified client application you can create with the SDK.
Additionally, we recommend checking our OPC UA Browser and OPC UA Simulation Server. The Browser serves as a generic graphical OPC UA Client and the Simulation Server is an OPC UA Server you can test against.
The server editions of the SDK also include a SampleConsoleServer
equivalent to the SampleConsoleClient
.
4. UaClient Object
The UaClient class is the main class you will be working with. It encapsulates the connection to the OPC UA server and handles the various details of the actual OPC UA communications, thus providing you with a simple interface to access from your applications. These are the lines in the 'SampleConsoleClient.java' file that create the UaClient
object:
protected UaClient client;
...
client = new UaClient();
client.setAddress(serverAddress);
4.1. Server Connection
In the previous example, the serverAddress
argument defines the server you are connecting to. Some sample addresses are provided in the following table:
Address |
Server |
opc.tcp://<hostname>:52520/OPCUA/SampleConsoleServer |
Prosys OPC UA SDK for Java Sample Console Server |
opc.tcp://<hostname>:53530/OPCUA/SimulationServer |
|
opc.https://<hostname>:53443/OPCUA/SimulationServer |
Prosys OPC UA Simulation Server with OPC UA HTTPS, if that is enabled |
opc.tcp://uademo.prosysopc.com:53530/OPCUA/SimulationServer |
Publicly available instance of the Prosys OPC UA Simulation Server |
opc.tcp://localhost:48010 |
|
opc.tcp://<hostname>:62541/Quickstarts/DataAccessServer |
OPC Foundation QuickStart Data Access Server |
<hostname> is the hostname of the computer in which the server is running. If it is running on the same machine localhost
and 127.0.0.1
also typically work in addition to the hostname.
The servers define a list of endpoints that they are listening to. The actual hostname in the endpoint may differ from the one that you use for connection. For Windows hostname resolution, see http://technet.microsoft.com/en-us/library/bb727005.aspx. If you are using the client in Linux, you cannot use NetBIOS computer names to access Windows servers. In general, it is best to use TCP/IP DNS names from all clients. Alternatively, you can always use the IP address of the computer. |
The first part of the address defines the transport protocol to use. opc.tcp
refers to OPC UA TCP communication which is usually the preferred protocol. An alternative transport protocol is OPC UA HTTPS, which some servers may support as well, but in general it is not used very much as it does not offer real benefits to the highly optimized OPC UA TCP protocol. You can use opc.https
addresses to communicate with those servers.
OPC UA HTTPS uses HTTPS as a transport channel, but still requires OPC UA capable applications. So, it does not enable generic web clients to communicate with OPC UA servers. The OPC Foundation has defined also a WebSocket transport protocol alternative ( |
4.2. Reverse Connection
OPC UA Specification 1.04 defines a new way to open OPC UA TCP connections, called Reverse Connection. In this mode, the server application will open the connection, contrary to the normal connection opened by the client. This can be useful in situations where the server is behind a firewall that cannot let client connections go through to the server.
In order to enable the reverse connection, the client application will first open a TCP/IP socket in a custom port. The Server may then open the connection to the client socket. After that the client will create the OPC UA TCP secure channel and session to the server as usual, including all security details.
In UaClient
this can be achieved by defining the socket to be listened with either UaClient.setReversePort()
or UaClient.setReverseAddress()
. For example:
client.setReverseAddress(UaAddress.parse("opc.tcp://localhost:6000"));
Now, when you call connect()
, the client will go waiting for connections from servers. Note that you should not use UaClient.setAddress()
in this case.
You should also define a listener to validate incoming connections from the server using UaClient.setReverseConnectionListener()
. For example:
public class MyReverseConnectionListener implements ReverseConnectionListener {
@Override
public boolean onConnect(String serverApplicationUri, String endpointUrl, SocketAddress remoteAddress) {
/*
* You can perform here validation for reverse connections. Return false, if you wish to stop
* the connection. Note that the connection is already initiated by the server at the
* SocketAddress.
*
* NOTE! The endpointUrl parameter is sent by the Server and used in the Client in calls forming
* the higher level communication channel. It is not the actual address the client is connecting
* (as the socket is already open to the remoteAddress) and most of the time is one of the
* endpointUrls used in normal non-reverse connections for the Server.
*/
System.out.println("Accepting reverse connection to server: " + serverApplicationUri + " at: " + remoteAddress
+ " , using endpointUrl: " + endpointUrl);
return true;
}
}
At this point in the communication the channel is not encrypted. Therefore when possible you should validate that the server is within the list of known servers by checking the remote SocketAddress of the server. The server ApplicationURI and EndpointUrl are given by the server when they connect. You can use all the security options as in usual connections to validate that the client is communicating with a trusted server.
|
5. Security Settings
OPC UA applications enable full security that is integrated into the communications. You can decide in the client, which kind of security settings you want to use or make available in your application. Usually, the end user should be able to configure the security level of each connection according to his needs.
5.1. Application Identity
All OPC UA applications must define some characteristics of themselves. This information is communicated to other applications via the OPC UA protocol when the applications are connected.
For secure communications, the applications must also define an Application Instance Certificate, which they use to authenticate themselves to other applications they are communicating with. Depending on the selected security level, servers may only accept connections from clients that they trust.
5.1.1. Application Description
The characteristics of an OPC UA application is defined in the following method:
protected ApplicationDescription initApplicationDescription(String applicationName, ApplicationType applicationType) {
ApplicationDescription applicationDescription = new ApplicationDescription();
// 'localhost' in the ApplicationName and ApplicationURI is converted to the actual host name
// of the computer in which the application is run.
applicationDescription.setApplicationName(new LocalizedText(applicationName + "@localhost"));
// ApplicationUri defines a unique identifier for each application instance-. Therefore, we
// use the actual computer name to ensure that it gets assigned differently in every
// installation.
applicationDescription.setApplicationUri("urn:localhost:OPCUA:" + applicationName);
// ProductUri should refer to your own company, since it identifies your product
applicationDescription.setProductUri("urn:prosysopc.com:OPCUA:" + applicationName);
applicationDescription.setApplicationType(applicationType);
return applicationDescription;
}
which can then be called, with
[...]
ApplicationDescription applicationDescription = initApplicationDescription(APP_NAME, ApplicationType.Client);
ApplicationName is used in user interfaces as a name for each application instance.
ApplicationUri is a unique identifier for each running instance.
ProductUri, on the other hand, is used to identify your product and should therefore be the same for all instances. It should refer to your own domain, for example, to ensure that it is globally unique.
Since the identifiers should be unique for each instance (i.e. installation), it is a good habit to include the hostname of the computer in which the application is running in both the ApplicationName and the ApplicationUri. The SDK supports this by automatically converting localhost
to the actual hostname of the computer (e.g. 'myhost'). Alternatively, you can use hostname
, which will be replaced with the full hostname, including the possible domain name part (e.g. 'myhost.mydomain.com').
The URIs must be valid identifiers, i.e. they must begin with a scheme, such as ‘urn:’ and may not contain any space characters. There are some applications in the market, which use invalid URIs and may therefore cause some errors or warnings with your application. |
5.1.2. Application Instance Certificate
You can define the Application Instance Certificate for the client by setting an ApplicationIdentity
for the UaClient
object. The simplest way to do this is to use the loadOrCreateCertificate()
method:
final ApplicationIdentity identity = ApplicationIdentity.loadOrCreateCertificate(
appDescription,
"Sample Organisation",
privateKeyPassword,
privatePath,
issuerCertificate,
keySizes,
/* Enable renewing the certificate */true);
On the first run, it creates the certificate and the private key and stores them in the folder defined by privatePath
.
privateKeyPassword
may help protecting the key from misuse, but we leave it null by default.
issuerCertificate
is also null by default, in which case we are creating a self-signed certificate.
keySizes
is used to define the strength of the security keys. 2048 is the default in the sample and usually good enough.
The last parameter enables automatic certificate renewal when the certificate expires.
As the name refers, the certificate is used to identify each application instance. That means that on every computer, the application has a different certificate. The certificate contains the ApplicationUri, which also identifies the computer in which the application is run, and must match the one defined in the |
ApplicationIdentity can also be created with it’s constructors. You will need to load the certificate and private key separately and also set the If your application does not use security, you may also create the ApplicationIdentity without any certificate by using the default constructor. However, you should always define the |
Note that if some other application gets the same key pair, it can pretend to be the same client application. The private key should be kept safe in order to reliably verify the identity of this application. Additionally, you may secure the usage of the private key with a password that is required to open it for use (but you need to add that in clear text in your application code or prompt it from the user). The certificate is public and can be distributed and stored freely in the servers and anywhere else. |
The SDK stores the certificate into a file named '<ApplicationName>@<hostname>_<keysize>.der' and private key in a respective '.pem' file. If you get them from an external CA, you can just replace the files in the file system. Sometimes the private key can be provided in a '.pfx' (PKCS#12) file. The SDK can also use that if '.pem' is not present. '.pfx' file may sometimes include both the private key and the certificate, but the 'loadOrCreateCertificate' method does not support reading the certificate from it. OpenSSL can be useful for converting between different file formats - or extracting the certificate and private key from a '.pfx' file. |
5.1.3. Issuer Certificate
Instead of using self-signed certificates, it would be better to use certificates signed by a recognized Certificate Authority (CA). Often, this should be a CA managed by an administrator in the company that is using the applications. The idea is to define a trust between the applications and the CA helps centralizing the management of this trust.
The CA should be run securely, and the private key of the CA should never be exposed outside the CA computer.
For the purpose of this tutorial, we can however, create a sample CA certificate and use that for signing our Application Instance Certificate. In order to create a sample issuer certificate, you can use, for example:
KeyPair issuerCertificate =
ApplicationIdentity.loadOrCreateIssuerCertificate(
"ProsysSampleCA", privatePath, privateKeyPassword, 3650, false);
You can then use this in the loadOrCreateCertificate
call.
The self-made issuer key does not replace a real CA. In real installations, it is always best to establish a central CA and create all keys for the applications using the CA. In this scenario, you can copy the certificate of the CA to the trust list of each OPC UA application. This will enable the applications to automatically trust all keys created by the CA. |
5.1.4. Multiple Application Instance Certificates
OPC UA specification defines different security profiles, which may require different kind of Application Instance Certificates, for example with different key sizes. The SDK enables usage of several certificates by defining an array of keySizes, e.g.:
// Use 0 to use the default keySize and default file names (for other
// values the file names will include the key size.
int[] keySizes = new int[] { 2048, 4096 };
5.2. Security Modes
5.2.1. SecurityMode for OPC UA TCP
Once the certificates are defined, you may decide the level of security that is used in the OPC UA TCP binary communications by setting the SecurityMode
:
client.setSecurityMode(SecurityMode.NONE);
As implied by the name, this setting enables access without any security features. Whereas it is usually good to start practicing the communication without security, eventually, we recommend using OPC UA security, whenever possible.
The servers will actually decide, which security modes they support. So, if you really need to make your network secure, you can consider disabling the SecurityMode None in the servers.
The SecurityMode
class contains several secure alternatives that you can choose from. It is actually a combination of MessageSecurityMode
and SecurityPolicy
.
MessageSecurityMode
is always either None, `Sign
or SignAndEncrypt
. Sign
adds a digital signature to every message, ensuring that the message contents cannot be modified during transfer. SignAndEncrypt
also encrypts the contents so that they cannot be read by third parties that might be listening to the traffic in the network.
SecurityPolicy
is a set of security algorithms that is defined in the OPC UA Specification. It has been changing over the years, so that the two original policies, Basic128Rsa15
and Basic256
have already been deprecated from the latest specifications, due to some details. Basic256Sha256
was added in OPC UA 1.03 and is currently the most commonly supported secure policy. The new policies, Aes128_Sha256_RsaOaep
and Aes256_Sha256_RsaPss
were defined in OPC UA 1.04 to make the choices more future proof. The 128
and 256
refer to the size of symmetric encryption keys - the shorter 128-bit keys make communication faster, but this is seldom a real concern.
If you wish to stick to a certain SecurityMode
, you can use the constant values defined in the SDK as follows:
client.setSecurityMode(SecurityMode.BASIC256SHA256_SIGN);
or
client.setSecurityMode(SecurityMode.BASIC256SHA256_SIGN_ENCRYPT);
But in most applications, you should leave this user configurable. The SampleConsoleClient
also enables you to change it from command line, so we are initializing it with a variable:
client.setSecurityMode(securityMode);
Although, the |
In practice, you can use only security modes that are enabled in the server that you are connecting to. If you don’t know which they are, you can call
client.getSupportedSecurityModes();
5.2.2. HTTPS SecurityPolicy
If you use the OPC UA HTTPS transport protocol for Server Connection the UaClient
will negotiate a usable TLS security policy with the server application. You can define which policies your application supports with
// The TLS security policies to use for OPC UA HTTPS
Set<HttpsSecurityPolicy> supportedHttpsModes = new HashSet<HttpsSecurityPolicy>();
// OPC UA HTTPS was added in UA 1.02
supportedHttpsModes.addAll(HttpsSecurityPolicy.ALL_102);
supportedHttpsModes.addAll(HttpsSecurityPolicy.ALL_103);
supportedHttpsModes.addAll(HttpsSecurityPolicy.ALL_104);
supportedHttpsModes.addAll(HttpsSecurityPolicy.ALL_105);
client.getHttpsSettings().setHttpsSecurityPolicies(supportedHttpsModes);
The constants ALL_102
, ALL_103
, ALL_104
and ALL_105
define which TLS security policies were considered safe in which OPC UA specification versions.
In order to be able to make a connection with OPC UA HTTPS, you must also be able to validate the HTTPS certificates properly. See Validating HTTPS Certificates for details about that.
In general, OPC UA HTTPS is quite tricky in practice, and it is not available in most applications, so only use it if you really need to. Usually you should do just fine with OPC UA TCP.
5.3. User Identity
In addition to verifying the identity of the applications, OPC UA also enables verification of user identities. In UaClient
, you can define the identity of the user with the UserIdentity
class. The SampleConsoleClient
does not do that by default, as each server defines what kind of user identities it supports. You can define a user identity that uses a standard username and password combination
client.setUserIdentity(new UserIdentity("my_name", "my_password"));
Another alternative is to use a certificate and private key, similar to the application instance identity, or a WS-SecurityToken provided by an external security system (e.g. SAML or Kerberos). To find out which user token types are supported by the server, call
client.getSupportedUserIdentityTokens();
5.4. Validating Server Certificates
An integral part of all OPC UA applications, in addition to defining their own security information, is of course, to validate the security information of the other party.
To validate the certificates of OPC UA servers, you need to define a CertificateValidator
in the UaClient
. This validator is used to validate the certificates received from the servers automatically.
To provide a standard certificate validation mechanism, the SDK contains a specific implementation of the CertificateValidator
, the DefaultCertificateValidator
. You can create the validator as follows:
// Use PKI files to keep track of the trusted and rejected server
// certificates...
final PkiDirectoryCertificateStore certStore = new PkiDirectoryCertificateStore();
final DefaultCertificateValidator validator = new DefaultCertificateValidator(certStore);
client.setCertificateValidator(validator);
The way this validator stores the received certificates is defined by the certStore
, which in the example is an instance of PkiDirectoryCertificateStore
. It keeps the certificates in files in specific directories, such as
'PKI/CA/certs' and 'PKI/CA/rejected'. The trusted certificates are stored in the 'certs' folder and the untrusted in 'rejected'. By default, the certificates are not trusted so they are stored in 'rejected'. You can then manually move the trusted certificates to the 'certs' directory.
Additionally, you can plug in a custom handler to the Validator by defining a ValidationListener
:
validator.setValidationListener(validationListener);
private static DefaultCertificateValidatorListener validationListener = new MyCertificateValidationListener();
where
/**
* A sampler listener for certificate validation results.
*/
public class MyCertificateValidationListener implements DefaultCertificateValidatorListener {
@Override
public ValidationResult onValidate(Cert certificate, ApplicationDescription applicationDescription,
EnumSet<CertificateCheck> passedChecks) {
// Called whenever the PkiFileBasedCertificateValidator has
// validated a certificate {
[...]
}
The SampleConsoleClient application uses the listener to prompt the user how previously untrusted certificates should be treated. The user can accept the certificate permanently, just once or reject it. In the first case, the certificate is placed in the 'certs' folder automatically, and in the latter cases, it is placed in the 'rejected' folder. In the last case, connection to the server is cancelled due to the certificate rejection.
You are, of course, free to use the listener to define any custom logic, but in principle, you should only trust certificates for which passedChecks
equals CertificateCheck.COMPULSORY
. Although self-signed certificates should not be used in proper onsite installations, that check is not included in the COMPULSORY
definition. Most OPC UA certificates are still self-signed, because they are easy to generate automatically. A proper Certificate Authority should be preferred in real systems, though, to enable a proper, centrally administered certificate management.
In addition to the checks provided by the CertificateValidator, the client should also verify that the hostname (or IP address) of the connection address is found from the certificate DNS names (in Subject Alternative Names). This is similar to how web browsers validate HTTPS connections: they trust a set of known CA certs, validate that the web site had a certificate signed by one of those well known CAs and also check that was the certificate for that site. |
The hostname check for OPC UA Application Instance Certificates is used for a similar purpose. But in practice this is only really useful when CA certs are used (as should be the case in real world installations). The SDK does not provide any implementation for this check, yet, so it must be done on the application level. See MyCertificateValidationListener.validateHostnameOfCertificate(Cert)
for details. Also, note that the users of your application should be able to suppress or disable this check, for use cases where it does not apply.
5.4.1. Validating HTTPS Certificates
OPC UA HTTPS connections are secured on the transport level using HTTPS Certificates (TLS certificates, in practice). Only the clients need to validate the servers' HTTPS Certificates.
In addition, the Application Instance Certificates may be used to authenticate OPC UA applications as with OPC UA TCP. This will happen, if the MessageSecurityMode
is not None
. Since messages are always encrypted on the transport level, it is enough to use MessageSecurityMode
Sign
to enable application authentication.
In order to validate the servers' HTTPS Certificates, you can plug in a Certificate Validator with HttpsSettings
like this:
client.getHttpsSettings().setHttpsCertificateValidator(validator);
You can use the same validator that you use to validate Application Instance Certificates with OPC UA TCP (see above).
Typically, HTTPS certificates are signed with CA certificates and some applciations only accept CA signed HTTPS certificates in general, so in order to trust a HTTPS certificate of a server, the respective CA certificate must be trusted first.
5.5. Teach Yourself the Security Details
OPC UA uses security heavily to guarantee that the applications can be safely used in real production environments. The security only works when configured properly, so you should make yourself familiar with the concepts and learn to configure these systems.
Read the OPC Unified Architecture book by Mahnke et al., for example, for more details on the OPC UA security settings and how they should be applied. The security technology follows standard PKI (Public Key Infrastructure) principles, so all material related to that can also be used to understand the basics.
Also, try different settings in different environments, so that you know more than you guess.
6. Connecting and Disconnecting
Once you have managed to get over the first compulsory hurdles of defining where and how to connect, you can simply connect to the server with
client.connect();
If that fails, you will get an exception. If the actual connection cannot be made, you will get a ServerConnectionException
. If you get a connection, but something goes wrong in the server, the UaClient
typically throws a ServiceException
. You may also see a ServiceFaultException
, ServiceResultException
or some other runtime exception, although they are typically coming from the lower level communication stack and should usually be handled by the SDK layer.
Often the original exception from the communication stack is available as the |
Once you have the connection, you can start playing with the server.
Once your application is finished and is ready to disconnect from the server, you can disconnect the client by simply calling
client.disconnect();
When running the |
6.1. Connection Monitoring
Each service call that you make to the server can fail, for example, if the connection is lost due to network problems or the server is simply shut down.
6.1.1. ServiceException
The service calls (described in the following sections) raise a ServiceException
in case of communication or other service errors.
6.1.2. Timeout
The SDK handles temporary communication breaks by automatically re-establishing a lost connection. Every service call is also monitored, and if the response takes longer than a client defined timeout, a ServiceException
with Bad_Timeout
is thrown. You can define the default timeout (in milliseconds) to use in the UaClient
with:
client.setTimeout(30000);
6.1.3. Server Status Monitoring
When connected to a server, the UaClient
periodically monitors the value of ServerStatus, which is a compulsory Object in the OPC UA Server Address Space. It will perform a check every StatusCheckInterval
(1 second by default). The client uses a specific timeout setting, StatusCheckTimeout
(10 seconds by default) to detect communication breaks.
You can listen to changes in the status by defining your own ServerStatusListener
, for example as follows:
/**
* A sampler listener for server status changes.
*/
public class MyServerStatusListener implements ServerStatusListener {
@Override
public void onShutdown(UaClient uaClient, long secondsTillShutdown, LocalizedText shutdownReason) {
// Called when the server state changes to Shutdown
SampleConsoleClient.printf("Server shutdown in %d seconds. Reason: %s\n", secondsTillShutdown,
shutdownReason.getText());
}
@Override
public void onStateChange(UaClient uaClient, ServerState oldState, ServerState newState) {
// Called whenever the server state changes
SampleConsoleClient.printf("ServerState changed from %s to %s\n", oldState, newState);
if (newState.equals(ServerState.Unknown)) {
SampleConsoleClient.println("ServerStatusError: " + uaClient.getServerStatusError());
}
}
@Override
public void onStatusChange(UaClient uaClient, ServerStatusDataType status, StatusCode code) {
// Called whenever the server status changes, typically every
// StatusCheckInterval defined in the UaClient.
// SampleConsoleClient.println("ServerStatus: " + status + ", code: " + code);
}
}
You can then set the client to use the new listener with:
protected ServerStatusListener serverStatusListener = new MyServerStatusListener();
...
client.addServerStatusListener(serverStatusListener);
6.1.4. Automatic Reconnect
The UaClient
enables automatic reconnections in case the communication fails. Whenever the status read fails due to a connection or timeout error or if the server notifies about a shutdown, the UaClient
will start to perform reconnect attempts every second in accordance to the procedure suggested in the OPC UA specifications.
If you wish to disable the automatic reconnect feature, call UaClient.setAutoReconnect(false)
. In this case, you can try to reconnect yourself by calling UaClient.reconnect()
until it succeeds.
7. OPC UA Server Address Space
OPC UA server applications provide all the data that they have available in their address space which is constructed in a standardised way according to the OPC UA Address Space Model. This helps client applications locate all relevant data from the server even if they don’t have any prior knowledge about it.
The OPC UA client applications identify data in the OPC UA server using Node Identifiers (NodeIds). NodeIds are used to uniquely identify all information in the address space which consists of Nodes that can be Objects, Variables or various Types. Nodes have Attributes that define their properties and, for example, contain the data that the Node provides (in the Value Attribute). Nodes are connected to each by References that signify their relationship, for example, the HasComponent Reference connects an Object and a Variable inside it. NodeIds are used when the client sends read or write requests to the server, for example. If the client applications don’t have the NodeId of a certain Node available, they can browse the server address space to find it.
You can use the Prosys OPC UA Browser application to explore the address space of any OPC UA server visually and access the information inside it.
7.1. Browse the Address Space
Typically, the first thing to do is to find the server items you wish to read or write. The OPC UA address space is a bit more complex structure than you might expect, but nevertheless, you can explore it by browsing.
In the UaClient
, the address space is accessed through the AddressSpace
property. You can call browse()
to request Nodes from the server.
The SampleConsoleClient
uses an internal variable, called nodeId
to keep track of the "Current Node" that is the target of all client operations. The nodeId
is initialized to the value Identifiers.RootFolder
, which is a standard NodeId
defined in the OPC UA specification. It corresponds to the root of the address space, which all servers must support. In addition, the servers must provide three standard subfolders under the RootFolder
: ObjectsFolder
, TypesFolder
and ViewsFolder
. You can start browsing from one of these to dynamically explore the available data and metadata (such as types) available from the server.
The |
So, in order to browse the address space with the SampleConsoleClient
, you start from the RootFolder
and follow References between the Nodes. There may be a huge number of References from a Node, so you can define some communication limits to the server. You can set these with the different properties of the AddressSpace
, e.g.:
client.getAddressSpace().setMaxReferencesPerNode(1000);
client.getAddressSpace().setReferenceTypeId(Identifiers.HierarchicalReferences);
by which you define a limit of 1000 References per call to the server and that you only wish to receive the hierarchical References between the Nodes.
The |
Now, if you call
List<ReferenceDescription> references = client.getAddressSpace().browse(nodeId);
you will get a list of ReferenceDescription
entries from the server. From these, you can find the target Nodes, which you can browse next. In the SampleConsoleClient
, you may choose which Node to browse next, or to end browsing and stay at the Node you are at that point. Check the sample code to see the specifics of the methods that are used to let you browse around the address space.
7.2. Browsing Through the Nodes
An alternative way to browsing using specific NodeIds is to follow the references between Node objects. You can access the References simply with node.getReferences()
, for example.
See the section Using Node Objects for more about that.
7.3. Translate BrowsePath to NodeId(s)
OPC UA has also a specific service that enables you to find the NodeId of a node by following a BrowsePath - a sequence of references from another node. This is most often used to locate components and properties of object and variable instances, corresponding to a known structure that is defined in the respective type definition. But, you can use it to find nodes, if you know their relative location anywhere in the address space.
The AddressSpace object has methods translateBrowsePathToNodeId and translateBrowsePathsToNodeIds that you can use. As input, you will need to prepare a list of RelativePathElement
structures and as a result you will get an array of BrowsePathTarget
structures. In the sample, we prompt the user for s path string and we split that using '/' characters. We also assume that the path should be followed using HierarchicalReferences
only. But in real, you can defined any references, for each RelativePathElement
.
String browsePathString = readInput(false);
List<RelativePathElement> browsePath = new ArrayList<RelativePathElement>();
for (String s : browsePathString.split("/")) {
final QualifiedName targetName = QualifiedName.parseQualifiedName(s);
browsePath.add(new RelativePathElement(Identifiers.HierarchicalReferences, false, true, targetName));
}
// The result may always contain several targets (if there are nodes with
// the same browseName), although normally only one is expected.
BrowsePathTarget[] pathTargets;
try {
pathTargets =
client.getAddressSpace().translateBrowsePathToNodeId(nodeId, browsePath.toArray(new RelativePathElement[0]));
for (BrowsePathTarget pathTarget : pathTargets) {
String targetStr = "Target: " + pathTarget.getTargetId();
if (!pathTarget.getRemainingPathIndex().equals(UnsignedInteger.MAX_VALUE)) {
targetStr = targetStr + " - RemainingPathIndex: " + pathTarget.getRemainingPathIndex();
}
println(targetStr);
}
} catch (StatusException e1) {
printException(e1);
}
8. Read Values
Once you have a Node selected, you can read the Attributes of the Node. There are actually several alternative read calls that you can make in the UaClient
. In SampleConsoleClient
we use the basic
DataValue value = client.readAttribute(nodeId, attributeId);
which reads the value of a single Attribute from a Node in the server. The Attribute to read is defined by the attributeId
. Valid IDs are defined in the Attributes
class. Note that different Node types (or NodeClasses according to the OPC UA terminology) support different Attributes. For example, the Attributes.Value
attribute is only supported by the Variable
and VariableType
Nodes.
In general, you should avoid calling the read methods for individual items. If you need to read several items at the same time, you should use readAttributes()
(to read several Attributes from one Node), readValues()
(to read the Value Attribute for several Variables) or consider using read()
. The read()
method is a bit more complicated to use, but it will only make a single call to the server to read any number of Attributes of any Nodes.
If you actually want to monitor Variables that are changing on the server, you had better use the Subscriptions, as described below in Subscriptions.
The method will throw ServiceException
or StatusException
if the call does fail (see Exceptions When Operations Fail for more information).
9. Write Values
Similar to reading, you can also write values to the server. For example:
boolean status = client.writeAttribute(nodeId, attributeId, value);
The method will throw ServiceException
or StatusException
if the call does fail (see Exceptions When Operations Fail for more information).
The return value will indicate if the write operation completes successfully and synchronously (true) or completes asynchronously (false).
Similar to the read methods, you also have better options for writing several values at the same time: writeAttributes()
, writeValues()
and the generic write()
.
10. Exceptions When Operations Fail
If any service call or operation fails, you will get an Exception
. For service call errors, such as when the server could not handle the service request at all, you can expect a ServiceException
. When performing a single operation, any failure (for example calling readAttribute()
with an invalid nodeId
or attributeId
or calling writeValue()
to a variable that does not permit changes) will produce a StatusException
.
If you perform several operations inside a single call (such as readValues()
), you can only expect a ServiceException
. For each operation you will get a StatusCode
that indicates which individual operation succeeded and which failed. Use StatusCode.isBad()
and .isGood()
to check whether the operation failed or not. The StatusCode
provides a complete status code, which you can check against all status codes defined in the OPC UA specification. In case of failure, you may also get additional information in a DiagnosticInfo
structure. These fields are present in the exceptions. You can also examine the result codes of the last service call from client.getLastServiceDiagnostics()
and getLastOperationDiagnostics()
.
|
11. Subscriptions
In addition to reading and writing, OPC UA defines a mechanism to monitor data changes and events via subscriptions. In order to do that, you will have to create the respective Subscription
instances and add them to the client.
The subscriptions use monitored items to define individual variables or objects that you can monitor for data changes or events, respectively.
11.1. Subscribe to Data Changes
To monitor data changes in any Variable, you can use a MonitoredDataItem
. For example:
subscription = new Subscription();
MonitoredDataItem item = new MonitoredDataItem(nodeId, attributeId, MonitoringMode.Reporting);
subscription.addItem(item);
client.addSubscription(subscription);
The subscription defines the default monitoring properties, namely the PublishingInterval that are shared by all items. Alternatively, the items may define individual details through their own properties.
Once the subscription and monitored items are defined, you can just listen to data change notifications via a DataChangeListener
:
item.setDataChangeListener(dataChangeListener);
11.1.1. DataChangeListener
The listener is defined as follows:
protected MonitoredDataItemListener dataChangeListener = new MyMonitoredDataItemListener(this);
where
/**
* A sample listener for monitored data changes.
*/
public class MyMonitoredDataItemListener implements MonitoredDataItemListener {
private final SampleConsoleClient client;
public MyMonitoredDataItemListener(SampleConsoleClient client) {
this.client = client;
}
@Override
public void onDataChange(MonitoredDataItem sender, DataValue prevValue, DataValue value) {
SampleConsoleClient.println(client.dataValueToString(sender.getNodeId(), sender.getAttributeId(), value));
}
};
You can use the same listener with all your items, of course.
Alternatively, you can use a listener in the Subscription
instance, to get notifications of complete messages sent by the server. The SampleConsoleClient
demonstrates both alternatives, but uses mainly the item-based listener.
11.2. Subscribe to Events
In order to listen to events in Event Notifier Objects, you can use MonitoredEventItems
.
Basically, all objects that send events, should have the SubscribeToEvents
bit set in their EventNotifier
attribute. But, since not all servers obey this rule, you can always try to monitor any object and we also ignore that in our example.
First of all, you must define an EventFilter
that specifies more details on which events and which data from them you wish to monitor. To start, you must define the SelectClauses
for the filter. They will specify the event fields that you wish to monitor. Additionally, you may also define WhereClauses
, which you can use to actually choose which exact events you wish to receive.
In the SampleConsoleClient, we have defined the event fields using a custom manner as follows:
protected final QualifiedName[] eventFieldNames = {
new QualifiedName("EventType"), new QualifiedName("Message"),
new QualifiedName("SourceName"), new QualifiedName("Time"),
new QualifiedName("Severity"), new QualifiedName("ActiveState/Id") ,
null, null
};
The last two fields are left as place holders for two custom fields. These are initialized at run-time, because the QualifiedName-identifiers need a |
The |
11.2.1. SelectClauses
So, we can define SelectClauses
for the filter like this:
NodeId eventTypeId = Identifiers.BaseEventType;
UnsignedInteger eventAttributeId = Attributes.Value;
String indexRange = null;
SimpleAttributeOperand[] selectClauses = new SimpleAttributeOperand[eventFields.length + 1];
for (int i = 0; i < eventFields.length; i++) {
QualifiedName[] browsePath = createBrowsePath(eventFields[i]);
selectClauses[i] =
new SimpleAttributeOperand(eventTypeId, browsePath, eventAttributeId, indexRange);
}
// Add a field to get the NodeId of the event source
selectClauses[eventFields.length] =
new SimpleAttributeOperand(Identifiers.ConditionType, new QualifiedName[0], Attributes.NodeId,
null);
// Create the filter
EventFilter filter = new EventFilter();
// Select the event fields
filter.setSelectClauses(selectClauses);
11.2.2. WhereClause
And next we filter the events we want to receive using the WhereClause
(this is optional - if you don’t define the WhereClause, you will receive all events without further filtering):
// Event filtering: the following sample creates a
// "Not OfType GeneralModelChangeEventType" filter
ContentFilterBuilder fb = new ContentFilterBuilder();
// The element operand refers to another operand -
// operand #1 in this case which is the next,
// LiteralOperand
fb.add(FilterOperator.Not, new ElementOperand(
UnsignedInteger.valueOf(1)));
final LiteralOperand filteredType = new LiteralOperand(
new Variant(Identifiers.GeneralModelChangeEventType));
fb.add(FilterOperator.OfType, filteredType);
filter.setWhereClause(fb.getContentFilter());
This one just filters out possible ModelChangeEvents. There are various operators that you can use. Most of them require two arguments, for example fb.add(FilterOperator.Equals, operand1, operand2)
.
11.2.3. MonitoredEventItem
So, finally we are ready to create the event item using the filter
that was just created and the NodeId of the object we wish to monitor:
MonitoredEventItem eventItem = new MonitoredEventItem(nodeId, filter);
eventItem.addEventListener(eventListener);
subscription.addItem(eventItem);
If you don’t know any better, you can always monitor the Server
object (Identifiers.Server
), in which case you will receive all events from the server.
The event listener is defined as follows, and used to react to the event notification:
protected final MonitoredEventItemListener eventListener = new MyMonitoredEventItemListener(this, eventFieldNames);
where
/**
* A sampler listener for monitored event notifications.
*/
public class MyMonitoredEventItemListener implements MonitoredEventItemListener {
private final SampleConsoleClient client;
private final QualifiedName[] requestedEventFields;
/**
* @param client
* @param eventFieldNames
*/
public MyMonitoredEventItemListener(SampleConsoleClient client, QualifiedName[] requestedEventFields) {
super();
this.requestedEventFields = requestedEventFields;
this.client = client;
}
@Override
public void onEvent(MonitoredEventItem sender, Variant[] eventFields) {
SampleConsoleClient.println(client.eventToString(sender.getNodeId(), requestedEventFields, eventFields));
}
};
11.2.4. Alarms and Condition Refresh
OPC UA alarms are also monitored via events. If you expect alarm notifications from the server, you should also use the ConditionRefresh method to request the currently active alarms to be sent, after you have created a MonitoredEventItem. The SDK enables this simply with
subscription.conditionRefresh();
If the server supports the service, you should see something like this in the SampleConsoleClient:
Node: i=2253 Fields: EventType=RefreshStartEventType {i=2787}; Message=Refresh Start; SourceName=Server; Time=09/06/23 15:19:40.9460000 GMT; Severity=1; ActiveState/Id=null; MyVariable=null; MyProperty=null; ConditionId=i=0 Node: i=2253 Fields: EventType=ExclusiveLevelAlarmType {i=9482}; Message=Level exceeded; SourceName=MyLevel; Time=09/06/23 15:19:26.9300000 GMT; Severity=500; ActiveState/Id=false; MyVariable=null; MyProperty=null; ConditionId=2:MyLevelAlarm {ns=2;s=MyLevel.Alarm} Node: i=2253 Fields: EventType=RefreshEndEventType {i=2788}; Message=Refresh End; SourceName=Server; Time=09/06/23 15:19:40.9470000 GMT; Severity=1; ActiveState/Id=null; MyVariable=null; MyProperty=null; ConditionId=i=0
11.3. SubscriptionAliveListener
You may also wish to listen to the alive and timeout events in the subscription. These will help you verify that the server is actively monitoring the values, even in the case that the values are not actually changing and therefore new data change notifications are not being sent. The example below demonstrates how to add such a listener to a subscription:
protected SubscriptionAliveListener subscriptionAliveListener = new MySubscriptionAliveListener();
...
subscription.addAliveListener(subscriptionAliveListener);
The functionality related to different events must be implemented in the respective methods of the listener class:
/**
* A sample listener for subscription alive events.
*/
public class MySubscriptionAliveListener implements SubscriptionAliveListener {
private static final ZoneId TIME_ZONE = ZoneId.systemDefault();
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(TIME_ZONE);
private static void printInfo(String event, Subscription subscription) {
String lastAlive = subscription.getLastAlive() == null ? "null" : FORMATTER.format(subscription.getLastAlive());
SampleConsoleClient.println(String.format("%s Subscription %s: ID=%d lastAlive=%s", FORMATTER.format(Instant.now()),
event, subscription.getSubscriptionId().getValue(), lastAlive));
}
@Override
public void onAfterCreate(Subscription subscription) {
// the subscription was (re)created to the server
// this happens if the subscription was timed out during
// a communication break and had to be recreated after reconnection
printInfo("created", subscription);
}
@Override
public void onAlive(Subscription subscription) {
// the server acknowledged that the connection is alive,
// although there were no changes to send
printInfo("alive", subscription);
}
@Override
public void onLifetimeTimeout(Subscription subscription) {
printInfo("lifetime ended", subscription);
}
@Override
public void onTimeout(Subscription subscription) {
// the server did not acknowledge that the connection is alive, and the
// maxKeepAliveCount has been exceeded
printInfo("timeout", subscription);
}
}
11.4. Transferring Subscriptions Between Sessions and Automatic Reconnections
The Subscription is always attached to a client Session, but you can transfer the Subscription to another Session, if necessary.
The SDK typically handles connection breaks automatically and creates new sessions whenever required. It can also transfer the Subscription automatically to a new Session when required or even create a new Subscription with new MonitoredItems, if the Subscription also times out.
Additionally, you can create a new UaClient
instance and use the Subscription.transferTo(UaClient)
method to move the Subscription to the new client. It is recommended to connect the new client to the server first.
transferTo
will use the OPC UA TransferSubscription service to enable seamless continuation of monitoring in fail-over situations. The SDK only enables that within the same application, though, using the Subscription and MonitoredItems objects.
Should you need to move a Subscription to another, redundant, client application, you will need to exchange information about the MonitoredItems between the applications. This will enable creating a new Subscription with similar parameters in the other client, but not the TransferSubscription service.
12. History Access
OPC UA servers may also provide history information for the Nodes, including historical time series data and events.
12.1. Reading History
To actually read history data, you have several options. The basic way is to use UaClient.historyRead()
, which is recommended if you need to do several readings at once. This example reads a complete history for a single Node (specified by nodeId
):
HistoryReadDetails details = new ReadRawModifiedDetails(false,
DateTime.MIN_VALUE, DateTime.currentTime(),
UnsignedInteger.MAX_VALUE, true);
HistoryReadValueId nodesToRead = new HistoryReadValueId(
nodeId, null,
QualifiedName.DEFAULT_BINARY_ENCODING, null);
HistoryReadResult[] result = client.historyRead(details,
TimestampsToReturn.Both, true, nodesToRead);
HistoryData d = result[0].getHistoryData().decode();
DataValue[] values = d.getDataValues();
What you need to be aware of is that there are several “methods” that the historyRead()
actually supports, depending on which HistoryReadDetails
you use. For example, in the above example we used ReadRawModifiedDetails
, to read a raw history (the same structure is used to read modified history as well, therefore the name).
12.1.1. Reading Variable History
To make your life a bit easier, UaClient
also defines several convenience methods to make specific history requests.
First of all, you need to check, whether the variable has history:
if (variable.getUserAccessLevel().contains(AccessLevelType.Options.HistoryRead)) ...
This verifies that the variable has history and it is accessible with your current user account.
After this, you can use a few different methods.
To read the collected samples (raw history) for a certain interval, you can use historyReadRaw
:
DateTime endTime = DateTime.currentTime();
DateTime startTime = endTime.minus(30, ChronoUnit.MINUTES);
DataValue[] result = client.historyReadRaw(nodeId,
startTime, endTime, UnsignedInteger.valueOf(1000), true, null,
TimestampsToReturn.Source);
Here the UnsignedInteger.valueOf(1000)
defines the maximum number of values to request from the server per call. Eventually, it will try to read all values within the interval, but it can break the call to several requests using this parameter. Note that if the interval is long and there is a lot of data, this can also take quite long to perform.
Alternatively, you can read values corresponding to certain time stamps with historyReadAtTimes
:
DateTime[] reqTimes = new DateTime[] {startTime, endTime};
values = client.historyReadAtTimes(nodeId, reqTimes, null, TimestampsToReturn.Source);
The server should adjust the raw sample data to match with the exact time stamps, possibly by linear interpolation.
If the server supports Aggregate Functions (Average, Min, Max, Count, etc.), you can also use historyReadProcessed
. Check the SampleConsoleClient.readHistory
for a complete example.
12.1.2. Reading Event History
Event history can be read for Objects that have recorded history in the server. You can find that out, by checking whether their EventNotifier
attribute contains HistoryRead
.
if (object.getEventNotifier().contains(EventNotifierType.Options.HistoryRead)) ...
You must define an EventFilter
, the same way that you do when you Subscribe to Events. In the SampleConsoleClient, we have a method that can provide the filter:
EventFilter eventFilter = createEventFilter(eventFieldNames);
Then you can read the events using:
DateTime endTime = DateTime.currentTime();
DateTime startTime = endTime.minus(1, ChronoUnit.HOURS);
HistoryEventFieldList[] events = client.historyReadEvents(nodeId, startTime, endTime,
UnsignedInteger.valueOf(1000), eventFilter, TimestampsToReturn.Source);
12.2. Updating or Deleting History
To modify existing history data in the server, you can use the historyUpdate()
method or, again, one of the convenience methods that provide you with more semantics. See the documentation for the various historyUpdateXxx()
and historyDeleteXxx()
methods in UaClient
for more about those.
13. Calling Methods
OPC UA also defines a mechanism to call Methods in the server Objects.
To find out if an Object defines Methods, you can call
List<UaMethod> methods = client.getAddressSpace().getMethods(nodeId);
UaMethod
is a Node object, which gets stored into the NodeCache
(see Using Node Objects). If you wish to perform a light browse, you can just call:
List<ReferenceDescription> methodRefs = client.getAddressSpace().browseMethods(nodeId);
to get a list of the Method References from the Node.
The UaMethod
is initialized with the InputArguments
and OutputArguments
properties, which you can examine for the argument name, type, etc.
Argument[] inputArguments = method.getInputArguments();
Argument[] outputArguments = method.getOutputArguments();
To actually call the Method, you need to provide a valid value (as Variant
) for each of the InputArguments, and just call it:
Variant[] outputs = client.call(nodeId, methodId, inputs);
As a result you get values for the OutputArguments
.
Note also that you will usually need to use the DataTypeConverter
to convert the inputArguments
to the correct data type, before calling the Method. The OPC UA specification defines that the server may not convert the arguments, if they are provided with incorrect data types. So you will get Bad_InvalidArgument
errors for each argument that is not provided in the correct data type. See the sample code (inside SampleConsoleClient.readInputArguments()
) for more details.
14. Registering and Unregistering Nodes
These services are meant for improved performance. You can request the server to prepare some Nodes to which you will refer often in your client application by registering them with the RegisterNodes service call. The server may also define new and more efficient NodeIds for the Nodes and the client can then use the new NodeIds instead of the NodeIds it received by browsing the address space.
You can access these from the AddressSpace
. To register a Node for quick access, call:
NodeId[] registeredNodeId = client.getAddressSpace().registerNodes(nodeId);
When you are done, you can unregister the Nodes using, for example:
NodeId[] nodes = client.getAddressSpace().unregisterAllNodes();
These methods are not usually necessary and not always supported by the servers anyway, so you can usually ignore them. If the server manufacturer suggests, you could consider using them. |
15. Using Node Objects
The AddressSpace
object in the UaClient
can also cache Nodes on the client side. These UaNode
objects will help you to browse the address space and to use the information in your application more conveniently.
You can simply request the Node objects from the address space using the methods getNode()
, getType()
, getMethods()
, etc.
To see it in action, just go and explore the sample code in more detail – especially the methods printCurrentNode()
and referenceToString()
.
getNode()
is even more useful when used together with code generation. If you register generated classes, you can use complete UA types in your application through the respective Java classes. Read on to learn more about that.
16. Information Modeling and Code Generation
The Prosys OPC UA SDK for Java supports loading existing OPC UA information models in the standard OPC UA NodeSet2 XML files ('NodeSets' in the following text). It also supports code generation from the NodeSets, to create Java classes corresponding to the OPC UA Object and Variable Types. By default, type definitions from the Core Specification is already generated for the SDK.
You can use other information models respectively by importing type information from the NodeSets and by using the Code Generator to generate the respective Java classes to work with. Although this is mostly useful on the server side, you can also take advantage of these features in the client applications.
To create your own information models, you can use the OPC UA Modeler application.
16.1. Prosys OPC UA SDK for Java Code Generator
You can generate Java classes based on information models stored in the NodeSet files with the Code Generator provided with the Prosys OPC UA SDK for Java. The Code Generator is located in the 'codegen' folder of the distribution package.
For instructions on using the Code Generator provided with the Prosys OPC UA SDK for Java, please refer to the Code Generator Manual in the 'codegen' folder of the distribution package. |
Follow the instructions in the included manual and experiment with the samples to learn how to configure and execute the code generation procedure. Then you may return to this tutorial and read the following sections on how to utilize the generated classes in your own applications.
16.2. Using the Generated Classes in Applications
The Java classes generated with the Code Generator enable simple usage of the OPC UA information models. With the generated classes, the client application is able to handle the instances of the custom types in OPC UA servers as Java objects.
This section explains how the Java classes generated using the Code Generator can be imported and utilized in applications developed with the Prosys OPC UA Client SDK for Java.
16.3. Registering the Model
The generated Java classes for ObjectTypes and VariableTypes are more extended versions of the standard UaNode
implementations. In order to let the SDK use the generated classes, the SDK must be made aware of the generated classes. This is done by registering the model.
16.4. Using Instances of Generated Types
The client implementation classes are generated into 'client' sub-packages under the defined generation folders.
To use the generated Java classes in your applications:
-
Add the generated files to your project source path.
-
Register generated classes with
UaClient.registerModel(CodegenModel model)
. For example:client.registerModel(example.packagename.client.SampleClientInformationModel.MODEL);
The Code Generator has also an option for Automatic Model Registration. This is very useful, especially, when you are using a bigger number of models. Please see the Codegen Manual for more details.
-
Read instances from the server with
client.getAddressSpace().getNode(NodeId id)
. You have to cast the result to the correct generated type. Alternatively you can pass a Class parameter of the correct type as additional parameter, for example:AnalogItemType node = client.getAddressSpace().getNode(<NodeId>, AnalogItemType.class);
The SDK distribution package provides a sample information model in the 'SampleTypes.xml' file that is in
the 'models' folder (both the command line and Maven version of Codegen). A complete example of the procedure for using an instance of the ValveObjectType
from the 'SampleTypes.xml' model is demonstrated in the following example:
// 1. Register the generated classes in your UaClient object by
// using the SampleClientInformationModel class that is generated in the client package.
client.registerModel(example.packagename.client.SampleClientInformationModel.MODEL);
// 2. Get a node from the server using an AddressSpace object.
// Give the nodeId of the instance as a parameter.
// Cast the return value to the corresponding generated class
ValveObjectType sampleValve =
(ValveObjectType) client.getAddressSpace().getNode(nodeId);
// 3. Use the instance.
// e.g. get the cached value of the PowerInput Property
sampleValve.getPowerInput();
Calling the getter for component values will internally get the node from the AddressSpace and get the value from it. It does not directly make a read call to the server, unless the node is not present in the AddressSpace 's NodeCache . You need to use read() methods in the UaClient if you need the latest values.
|
Calling the setter for component values sets the value to the local node, i.e. it is not written to the server. You need to use write() methods in the UaClient to write the value to the server.
|
17. Custom DataTypes
OPC UA Servers enable flexible rules for defining custom data types. These can be used in OPC UA Information Models for different use cases. The servers will expose all of their type definitions so that client applications can also interpret the custom data types automatically.
Custom data types can be defined as Simple Types, Structures, Enumerations or OptionSets.
17.1. Simple Types
The OPC UA builtin types define how to communicate integers and floating point values of different precision, strings, byte arrays (ByteStrings) and a few custom OPC UA types, such as LocalizedText
, QualifiedName
etc.
All of these can be extended in practice, but for the simple types the values don’t really change and the custom types only define more refined semantics.
17.2. Structures
The OPC UA specification defines a DataType called Structure
that can represent any complex datatype. The custom structures are all extending this and they can consist of any number of Fields that can be of any data type. Fields can also be arrays of any data type.
17.3. Enumerations
Enumeration
types define a fixed set of values. They are transferred as Int32 (i.e. Integer
) on the wire and thus you will get it as such from UaClient.readValue()
, for example.
17.4. OptionSets
OptionSet
types are split into 2 categories, but both represent bitsets; collections of Boolean on/off flags. There are both 'Structure OptionSets' and 'UInteger OptionSets'.
'Structure OptionSets' are subtypes of the OptionSet
structure. 'UInteger OptionSets' are subtypes of any UInteger
type (Byte, UInt16, UInt32, UInt64) and they have an OPC UA Property called OptionSetValues
.
17.5. UaDataTypeSpecification
In order to enable processing the metadata and values of the different types, including all simple types, the SDK defines a Java interface UaDataTypeSpecification
, that has specific implementation for each variety, as follows:
UaDataTypeSpecification implementation | Usage |
---|---|
OptionSetStructureSpecification |
Models |
OptionSetSpecification |
Models OptionSets |
StructureSpecification |
Models Structures |
EnumerationSpecification |
Models Enumerations |
SimpleTypeSpecification |
Models simple types that are none of the above |
StructureSpecification and OptionSetSpecifications are interfaces, whereas the OptionSetStructureSpecification extends both.
|
SimpleTypeSpecification
has only a name, TypeId, BaseTypeId and the Java class corresponding to it. All simple types in OPC UA are encoded as their base type on the wire.
EnumerationSpecification
includes the alternative Values for the specific Enumeration
type.
OptionSetSpecification
includes the alternative Options for the OptionSet. They are modeled as OptionSpecification
.
StructureSpecification
includes all Fields for the Structure. They are modeled as FieldSpecification
.
OptionSetStructureSpecification
models both Fields (similar to the OptionSet
Structure, which means that new fields cannot be added to it) and Options.
The Enumeration
, UaOptionSet
, Structure
and OptionSetStructure
all have a method .specification()
that can be used to obtain the respective UaDataTypeSpecification
. Additionally they have API that allows generic processing. For example, Structure
has get/set
methods that can be used with a FieldSpecification
or field name. This allows building generic handling when reading the Value Attribute. Note that for Enumeration
the value is still a Integer
(as they are encoded as Int32), thus you must also read the DataType Attribute, and then find the correct EnumerationSpecification
from the EncoderContext
.
By default SDK reads metadata from the server as part of UaClient.connect
and forms the UaDataTypeSpecification
that represents the custom types. The Prosys OPC UA SDK for Java Code Generator also creates UaDataTypeSpecification
that SDK loads when the model is registered.
17.6. EncoderContext
Every UaClient
and UaServer
instance uses an EncoderContext
, which includes all known type specifications. UaClient
initializes them when it connects to the server as part of the TypeDictionary
initialization. Alternatively, the client can also access the type specifications directly from the DataTypeDefinition
attributes of the DataType nodes.