1. Introduction

Hello and welcome to OPC UA!

This is the Prosys OPC UA SDK for Delphi tutorial for client application development. With this quick introduction you should be able to grab the basic ideas behind the Prosys OPC UA SDK for Delphi.

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 full reference 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).

2. Sample Applications

The SDK contains a sample client application, ProsysOPC.UaSampleClient for Delphi and UaCppSampleClient for C++ . This tutorial will refer to the Delphi sample when it explains the main tasks of an OPC UA client. Unfortunately, the C++ sample is not as complete, so you will need to convert the Delphi code to C++ in order to accomplish the same.

3. TUaClient Component

TUaClient is the main component that you will be working with in your OPC UA client application. It encapsulates the connection to a single OPC UA server and handles the various details of the actual OPC UA communications. You can add a new TUaClient component either at design time in the form designer (see TUaSampleClientForm) or at run time as follows:

UaClient := TUaClient.Create(Owner);

where Owner is either nil or another component or form that will be responsible of freeing the component, as usual with Delphi components.

For each separate OPC UA server connection, you will have to use a separate UaClient component.

3.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. The certificates are used to authenticate OPC UA applications to each other. They are also used to provide the asymmetric keys (together with the respective Private Keys) which the applications use to sign and encrypt data. Depending on the selected security level, servers may only accept connections from clients that they trust.

3.1.1. Application Description

The characteristics of an OPC UA application are defined in the ApplicationDescription, which is part of the ApplicationIdentity:

UaClient.ApplicationIdentity.ApplicationDescription := TUaApplicationDescription.Builder
    .ApplicationUri('urn:localhost:UA:DelphiSampleClient')
    .ProductUri('urn:prosysopc.com:UA:DelphiSampleClient')
    .ApplicationName('DelphiSampleClient@localhost')
    .ApplicationType(atClient).Build;

where

ApplicationUri is a unique identifier for each running instance.

ProductUri 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.

ApplicationName is used in user interfaces as a name identifier for each application instance.

ApplicationType defines the type of the OPC UA application, which in this case is Client.

Since the ApplicationName and ApplicationUri 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 of them. The SDK supports this by automatically converting localhost to the actual hostname of the computer (e.g. 'myhost').

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.

3.1.2. Application Instance Certificate

In addition to ApplicationDescription, you have to define the Application Instance Certificate. By default, the applications can create their own certificate with the LoadOrCreateCertificate method:

UaClient.ApplicationIdentity.Organisation := 'Prosys OPC Ltd';
UaClient.ApplicationIdentity.LoadOrCreateCertificate({EnableRenew} false);

Organisation will be used for the Subject of the certificate, in addition to the ApplicationName that is defined in the ApplicationDescription. ApplicationUri from the ApplicationDescription will be added in the SubjectAlternativeName of the certificate.

LoadOrCreateCertificate creates a self-signed certificate and a corresponding private key for the application the first time it is called. It also stores them to the files '<ApplicationName>.der' and '<ApplicationName>.pem', respectively. On following runs, the application will load the certificate and the private key from these files. The Boolean parameter for LoadOrCreateCertificate determines whether the certificate will be automatically renewed when it expires.

As the name Application Instance Certificate says, the certificate is used to identify each application instance. That means that every installation on every computer must have a unique certificate. To ensure this, the certificate also contains the ApplicationUri, which must match the one defined in the ApplicationDescription. If you change the hostname of the computer, the certficate must also be recreated.

The self-signed certificates are useful to ensure that each application has a certificate by default. However, in real production environments, you should consider establishing a proper Certificate Authority (CA), which signs all application instance certificates. You can then manage them centrally and the applications can also trust automatically to each certificate that is created by the well-known CA. OPC UA also defines a product type, Glabal Discovery Server (GDS), which is designed to be such a CA. But at the moment, there are not many actual GDS products available in the market yet.

Note that if some other application gets the same key pair, it can pretend to be the same OPC UA 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 anywhere withouut affecting the security of the applications.

3.2. Validating Application Instance Certificates

As explained in the previous chapter, OPC UA applications define their identity with an ApplicationDescription and Application Instance Certificate.

Upon a secure connection, the applications verify each others' identity and they can also determine which applications they accept the connection with. This is done by validating the Application Instance Certificate.

By default, the SDK validates the certificate of the other application for basic validity, when the connection is made. This includes checking the validity period and that the ApplicationUri defined in the SubjectAlternativeNames field of the certificate matches the one in the ApplicationDescription provided at the connection phase.

To define which applications are allowed to have a connection, they use a Certificate Store, where the known Application Instance Certificates are stored. The Certificate Store includes a folder for trusted and rejected certificates and it may also include a folder for revoked certificates. The Certificate Store is in practice a folder on disk. The actual location is defined by the RootFolder property, which is by default 'PKI':

UaClient.CertificateStore.RootFolder := 'PKI';

There are also properties for configuring the locations of the trusted, rejected and revoked certificates, but these are also defined by default so you don’t need to touch them usually.

By default, a previously unseen certificate is not trusted and it is stored in the rejected folder. You can move it to the trusted folder to make it - and the respective application - trusted.

Certificates that are signed by a trusted CA, can be trusted automatically.

Client applications can also prompt the user at the connection phase to ask whether a new certificate can be trusted. This is possible with the OnValidate event:

UaClient.OnValidate := ValidateCertificate;

See the UaSampleClient for an example on how to implement a custom dialog that prompts the user whether a certificate should be trusted.

If the connection is made with MessageSecurityMode None, all security validation, including verification of the certificates is ignored. In practice, the applications do not then need to define any certificates either.

4. Server Connection

When you connect the client to an OPC UA server, you will have to define the ServerAddress that you wish to connect to. For example:

UaClient.ServerAddress := 'opc.tcp://localhost:62620';

Some sample addresses are provided in the following table:

URI

Server

opc.tcp://<hostname>:62620/

Prosys OPC UA SDK for Delphi Sample Server

opc.tcp://<hostname>:53530/OPCUA/SimulationServer

Prosys OPC UA Simulation Server

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.

The servers define a list of endpoints that they are listening to. The actual hostname in the EndpointUrl may differ from the one that you use for connection. For Windows hostname resolution, see http://technet.microsoft.com/en-us/library/bb727005.aspx. 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 UA TCP protocol, which is usually supported by all OPC UA applications. At the moment, the SDK does not support any other protocols.

5. Security Settings

OPC UA applications enable full security that is integrated into the communications. In the client you have to specify, which level of security should be used for each connection. Usually, the end user should be able to configure the security level of each connection according to his needs, so consider that an application level option in practice.

5.1. SecurityMode

The level of security for the UA TCP communications is defined by MessageSecurityMode, which has three alternative levels: None, Sign and SignAndEncrypt.

UaClient.MessageSecurityMode := msmNone;

This is the default setting, enabling access to the server without any security. It is usually best to first verify that the communication to a certain server can be established without security, but in general, you had better use secure communications unless you have a good reason to omit it. It is also possible that the server does not accept insecure connections.

To use secure communications, you can set for example:

UaClient.MessageSecurityMode := msmSign;

This will add digital signatures to every communication message, ensuring that no one can alter the communication on the wire.

Additionally, you can encrypt all messages and ensure that third parties cannot listen to the communication by using:

UaClient.MessageSecurityMode := msmSignAndEncrypt;

If MessageSecurityMode is Sign or SignAndEncrypt, SecurityPolicies will define the policies that the client may use for the secure connection. The known policies up to OPC UA 1.04 are: Basic128Rsa15, Basic256, Aes128Sha256RsaOaep, Basic256Sha256, and Aes256Sha256RsaPss. Basic128Rsa15 and Basic256 are not recommended any more due to some know vulnerabilities, but there may still be OPC UA servers that do not support any of the newer policies. SecurityPolicyStrategy will help to define the order of preference, when choosing the actual policy to use, and therefore enables the client to support the best available policy for each server without any further modifications.

By default, SecurityPolicies is initialized to AllSecurityPolicies, but if you wish to disable the deprecated polcies, you can initialize it to the 1.04 policies with

UaClient.SecurityPolicies := AllSecurityPolicies104;

5.2. User Identity

In addition to verifying the identity of the applications, OPC UA also enables user authentication per connection. In UaClient, you can use the UserIdentity property for this. for example, to use basic username and password authentication, you can use

UaClient.UserIdentity.SetUserNamePassword('opcua', 'opcua');

5.3. 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 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.

5.4. Connect and Disconnect

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

UaClient.Connect;

If that fails, you will get an exception.

Once you have the connection, you can start playing with the server. When running the sample client application, you are presented with a UI in which you can pick up different tasks.

To disconnect the client from the server, simply call

UaClient.Disconnect;

5.5. 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 if the server is shut down.

5.5.1. EUaServiceException

The service calls (described in the following sections) raise an EUaServiceException in case of communication or other service errors.

5.5.2. Timeout

The OPC UA Stack handles temporary communication errors by retrying to establish a lost connection in certain cases. It also takes care of timeout handling. That means that the synchronous service calls are monitored for a response until the timeout delay occurs without a response. You can define the default timeout (in milliseconds) to use in the UaClient with:

UaClient.OperationTimeout := 30000;

5.5.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, which is 10 seconds by default. StatusCheckTimeout defines how quickly a communication break is detected, if there is no response from the server. This is also 10 seconds by default.

You can listen to changes in the server state with the OnServerStateChange event:

procedure TUaSampleClientForm.ServerStateChange(Sender: TObject; OldState,
    NewState: TUaServerState);
begin
  EnableControls; // Refreshes the Status Display
end;

5.5.4. Automatic Reconnect

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 is shut down, UaClient will start to perform reconnect attempts according to the procedure suggested in the OPC UA specifications.

If you wish to disable the automatic reconnect feature, call UaClient.AutoReconnect := False;. In this case, you can try to reconnect yourself by calling UaClient.Reconnect until it succeeds.

6. Server Address Space

OPC UA servers provide the information that they have available from the OPC UA Server Address Space. This helps the client applications to locate all data that they need from the server, even when they don’t have any prior knowledge about it.

You can use Prosys OPC UA Client application or UaSampleClient to explore the address space of any OPC UA server visually.

6.1. Nodes

The OPC UA Server Address Space is composed of Nodes, which define all data and metadata of the server. The available Node Classes are Object, Variable, Method, ObjectType, VariableType, DataType, ReferenceType and View.

Objects are used for defining structures, whereas Variables are used for defining parameter and measurement data. They are both classified as Instances and they must always have a TypeDefinition, which refers to an ObjectType or VariableType, respectively.

The various types (ObjectType, VariableType, DataType and ReferenceType) are used to define the metadata. They are also the basis of OPC UA Information Models, which typically define standard or custom types, and help identifying predefined structures available in the server.

6.1.1. Namespace

OPC UA uses the concept of Namespace to help defining unique names and identifiers for the Nodes. BrowseName and NodeId attributes are defined with a reference to a Namespace. Different organizations can assign Namespaces for themselves and then define Nodes in these Namespaces to ensure that they are uniquely identified.

NamespaceUri

Each Namespace is identified by a NamespaceUri, which is typically in the format of 'http://<organization>/<NamespaceIdentifier>'. For example, the NamespaceUri of the standard OPC UA Namespace is 'http://opcfoundation.org/UA'.

NamespaceIndex

In OPC UA communication, applications typically refer to namespaces with a NamespaceIndex, instead of a NamespaceUri. This is done simply to improve the performance of communication. NamespaceIndex refers to the index of the respective Namespace in the NamespaceTable of the OPC UA server in question.

NamespaceTable

The servers use internal NamespaceTables to define the namespaces that they use. On the client side, the NamespaceTable is available from the TUaClient.NamespaceTable property.

Should you need to find the NamespaceIndex of a certain NamespaceUri, for example, you can always look for it from the NamespaceTable as follows:

Index := UaClient.NamespaceTable.IndexOfUri(NamespaceUri);

The servers expose the contents of the NamespaceTable via the NamespaceArray variable, which is a standard component of the Server object and available from the Address Space. The SDK reads the variable automatically at connection to a server. Should you need to refresh it later (in case the server may have changed the contents, for example), you can use UpdateNamespaceTable to refresh it.

TUaNamespace

In the OPC UA SDK for Delphi, we are using TUaNamespace objects to define the namespaces. The main property of TUaNamespace is NamespaceUri. You can get a TUaNamespace for a specific NamespaceUri with TUaNamespace.Get. For example:

Namespace := TUaNamespace.Get('http://www.prosysopc.com/UA/Delphi/SampleNamespace');

Respectively, you can find the index of a Namespace from the NamespaceTable as follows:

Index := UaClient.NamespaceTable.IndexOfUri(NamespaceUri);
OPC UA Standard Namespace

If you need a reference to the OPC UA Standard Namespace ('http://opcfoundation.org/UA/') you can use the variable OpcUaNamespace. It’s NamespaceIndex is always 0.

6.1.2. NodeId

The OPC UA client applications identify the nodes in the OPC UA server using Node Identifiers (NodeIds). They are used when the client sends any service requests related to nodes to the server, for example Read or Write. If the client application does not have the NodeId of a certain node available, it can browse the server address space to find it.

The NodeId is comprised of a Namespace and Identifier.

TUaNodeId

The SDK uses TUaNodeId records to define NodeIds. TUaNodeId always refers to a TUaNamespace using the Namespace property. This differs from most of the other toolkits and applications, which define NodeIds mainly with NamespaceIndex. This is troublesome since it requires knowledge of the NamespaceTable to find out the respective NamespaceUri and the index of a certain namespace may also change in theory over time (although in practice most servers try to ensure it doesn’t).

You can create a new NodeId with a Namespace and Identifier as follows:

Namespace := TUaNamespace.Get('http://www.prosysopc.com/UA/Delphi/SampleNamespace');
NodeId := TUaNodeId(Namespace, "MyLevel");

Or you can use ParseNodeId if you have the NodeId as string.

If you omit the Namespace, the NodeId will be defined in the OPC UA Standard Namespace.

NodeId as string

OPC UA does not define any official string format for displaying NodeIds, but many applications use the XML Encoding Format. This is defined as either

ns=<namespaceindex>;<type>=<value>
nsu=<uri>;<type>=<value>

where <namespaceindex> or <uri> defines the Namespace, <type> is either 'i', 's', 'g' or 'b' for UInt32, String, Guid or ByteString type of an identifier, respectively, and <value> is the respective identifier value.

TUaNodeId.ToString() uses the latter format to enable displaying the NodeId with the NamespaceUri.

If you wish to display a NodeId with the NamespaceIndex, you will need to use the NamespaceTable:

Namespace := TUaNamespace.Get('http://www.prosysopc.com/UA/Delphi/SampleNamespace');
NodeId := TUaNodeId(Namespace, "MyLevel");
Str := UaClient.NamespaceTable.NodeIdToString(NodeId, True);

which gives

'ns=2;s=MyLevel'

when used with the UaSampleServer, which has 'http://www.prosysopc.com/UA/Delphi/SampleNamespace' at Index=2 in the NamespaceTable.

ParseNodeId

Respectively, you can convert the NodeId back from the string representation with

NodeId := UaClient.NamespaceTable.ParseNodeId(Str);

It will try to convert from both NamespaceIndex and NamespaceUri formats.

6.1.3. QualifiedName

QualifiedName is the OPC UA data type used to define globally unique names, especially for the BrowseName attribute of Nodes. This is achieved by combining a string value with a Namespace in a similar manner that NodeId does for globally unique identifiers.

TUaQualifiedName

The SDK uses TUaQualifiedName records for defining QualifiedNames. Similar to TUaNodeId they also refer to a TUaNamespace instead of a NamespaceIndex.

You can construct a new TUaQualifiedName instance as follows:

Namespace := TUaNamespace.Get('http://www.prosysopc.com/UA/Delphi/SampleNamespace');
QualifiedName := TUaQualifiedName(Namespace, "MyLevel");

If you omit the Namespace, the QualifiedName will be defined in the OPC UA Standard Namespace.

QualifiedName as string

OPC UA does not define any official string format for displaying QualifiedName, but many applications use the format

<namespaceindex>:<name>

where <namespaceindex> defines the Namespace and <name> is the respective name. Contrary to NodeId, there is no string format in general use that would use the NamespaceUri instead of NamespaceIndex, unfortunately.

Nevertheless, TUaQualifiedName.ToString() uses the format 'nsu=<uri>;<name>' to enable displaying the QualifiedName with the NamespaceUri, similar to NodeIds.

If you wish to display a QualifiedName with the NamespaceIndex, you will need to use the NamespaceTable:

Namespace := TUaNamespace.Get('http://www.prosysopc.com/UA/Delphi/SampleNamespace');
QualifiedName := TUaQualifiedName(Namespace, "MyLevel");
Str := UaClient.NamespaceTable.UaQualifiedNameToString(QualifiedName);

which gives

'2:MyLevel'

when used with the UaSampleServer, which has 'http://www.prosysopc.com/UA/Delphi/SampleNamespace' at Index=2 in the NamespaceTable.

ParseQualifiedName

Respectively, you can convert the QualifiedName back from the string representation with

QualifiedName := UaClient.NamespaceTable.ParseQualifiedName(Str);

It will try to convert from both NamespaceIndex and NamespaceUri formats.

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

UaSampleClient has an AddressSpaceView that provides an interactive browser to the server address space. After the application is connected to a server, you should see three folders that every server should provide as standard browsing points: ObjectsFolder, TypesFolder and ViewsFolder. These are in fact subfolders to RootFolder, which is typically not visible, but which you can use as a single starting point for browsing the address space. You can start browsing from one of these to dynamically explore the data (Objects) and metadata (Types) that is available from the server.

All standard NodeIds are available from the UaBase unit and they start with Id_. For example, Id_RootFolder and Id_ObjectsFolder.

So, in order to browse the address space with the sample client, 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.:

UaClient.AddressSpace.MaxReferencesPerNode := 1000;
UaClient.AddressSpace.ReferenceTypeId := Id_HierarchicalReferences;

by which you define a limit of 1000 References per call to the server and that you only wish to receive Hierarchical References between the Nodes.

The AdressSpace will in fact use the MaxReferencesPerNode internally while communicating with the server. In practice, it will make subsequent calls to the server until it receives all the References, even in the case that the current Node has more References than defined by the limit. The limit is just necessary to avoid timeouts and too large messages in the communication.

Now, if you call

References := UaClient.AddressSpace.Browse(NodeId);

you will get an array of ReferenceDescription entries from the server as TArray<IUaReferenceDescription>. From these, you can find the target Nodes, which you can browse next. In UaSampleClient, you may use the AddressSpaceView to select the next Node to browse. Check the sample code to see the specifics of the methods that are used to browse the address space.

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 the sample client (TUaSampleClientForm.UpdateAttributesView) we use the basic method ReadAttributes as follows:

Values := UaClient.ReadAttributes(NodeId, Attributes);

which reads the values of multiple Attributes from a single Node.

See NodeId for details on how to create a valid NodeId.

Note that different Node types (or NodeClasses according to the OPC UA terminology) support different Attributes. For example, the aiValue attribute is only supported by the Variable and VariableType Nodes. There are also simplified methods ReadValue and ReadValues for reading the Value attribute of variables specifically.

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 Variable Nodes) 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 at once.

The read methods may throw EUaServiceException or EUaOperationException (single reads only). See Exceptions When Operations Fail for more details on individual reading errors.

If you actually want to monitor Variables that are changing on the server, you had better use the Subscriptions, as described below in Subscribe to Data Changes.

8.1. DataValue

OPC UA read and write service deal with a data type called DataValue. It is comprised of the actual Value, StatusCode and timestamps: one called SourceTimestamp and one called ServerTimestamp. The timestamps have also an extra field for picosecond level of accuracy.

8.1.1. Value as Variant

OPC UA uses a so called Variant data type for transmitting attribute values of any data type between the server and client. It resembles the Windows Variant, which Delphi supports natively, but is a bit different. Therefore, the OPC UA SDK defines a new type, TUaVariant, which is used for dealing with OPC UA Variant values. TUaVariant supports implicit conversions between different native data types - and also Delphi Variants, but when you want to be sure of your conversions, you can use the explicit Create methods to construct new values. You can also use the various AsXxx methods to read the value as Xxx data types our of the variant. You can use BuitinType or VarType to find out the data type of the value.

Reading structure types

OPC UA has an extensible data type called Structure, which enables custom structural data types to be defined by the applications. There are also a number of standard structural types, for example ServerStatusDataType (the recommendation is that the names of these types end with 'DataType').

The BuiltinType bitExtensionObject refers to values that are sent as encoded ExtensionObjects over the OPC UA communication channel (officially called SecureChannel). In practice, these correspond to different Structure data types - and you will find the types supported by the server in the address space under Types/DataTypes/Structure.

The OPC UA SDK decodes the structures automatically from ExtensionObjects and they are always implementing IUaStructure. Every standard structure type (defined in Opc.Ua.Nodeset2.xml) also has its own specific type generated into the SDK. So, for example for the ServerStatusDataType there is a respective IUaServerStatusDataType interface, which you can use to get access to the specific fields.

So, if you know that the data type is ServerStatusDataType, you can use the value as follows:

var
  DataValue: TUaDataValue;
  StatusCode: TUaStatusCode;
  ServerStatus: IUaServerStatusDataType;
begin
    DataValue := UaClient. ReadValue(Id_Server_ServerStatus);
    StatusCode := DataValue.StatusCode;

    if StatusCode.IsGood then
      ServerStatus := DataValue.Value.AsStructure as IUaServerStatusDataType;
...

For possible custom structures that are found in the server, but which the client doesn’t know of beforehand, we cannot (yet) generate any specific types. The SDK can, however, provide them as IUaDynamicStructure instances. It enables you to break the structure into field names and values that you can use accordingly.

var
  DataValue: TUaDataValue;
  StatusCode: TUaStatusCode;
  DynamicStructureValue: IUaDynamicStructure;
  Field1Value: TUaVariant;
begin
    DataValue := UaClient. ReadValue(Id_Server_ServerStatus);
    StatusCode := DataValue.StatusCode;

    if StatusCode.IsGood then
    begin
                  DynamicStructureValue := Client.ReadValue(DynamicStructureVariable.NodeId).Value.AsStructure as IUaDynamicStructure;
                  Field1Value := DynamicStructureValue.FieldValues['Field1'];
...

8.1.2. StatusCode

StatusCode tells whether the data is valid and if not, the code tells the reason. All known StatusCodes are available as constants that begin with Good_, Bad_ or Uncertain_, for example Bad_UserAccessDenied.

8.1.3. SourceTimestamp and ServerTimestamp

OPC UA defines two timestamps for value changes. SourceTimestamp reflects the time when the value was changed in the measurement system as well as possible, whereas ServerTimestamp defines when the server was last aware of the value being valid. Usually, when you are using the data, SourceTimestamp is the more important one.

When you are reading, you have an optional argument that you can provide, called MaxAge. It instructs the server to go acquiring a new value if the ServerTimestamp of the latest sample is older than that.

9. Write Values

Similar to reading, you can also write values to the server. For example:

Status := UaClient.WriteAttribute(NodeId, AttributeId, Value);

Please, refer to Read Values for details about NodeId and AttributeId. Value must be a TUaDataValue or TUaVariant, but see DataType Conversions below to ensure you provide it in the correct data type, anyway.

Instead of WriteAttribute, you can call `WriteValue to write to the value attribute of a Variable. WriteAttribute and WriteValue will throw EUaServiceException if the service call fails or EUaOperationException if the server does not accept the write request. (see Exceptions When Operations Fail for more information).

The return value will indicate if the write operation completes synchronously (true) or asynchronously (false).

Similar to the read methods, you also have better options for writing several values at the same time: WriteAttributes, WriteValues, and also the most generic Write method. They will throw exceptions only when the whole service call fails. Individual operation failures will be indicated in the results that are returned.

9.1. DataType Conversions

When you are writing, you will have to ensure that the values are converted to the expected data type. In general, your application may know the data types before hand, but if this is not the case, you can rely on Variant type conversions. For example as is done in the UaSampleClient:

procedure TUaSampleClientForm.WriteActionExecute(Sender: TObject);
var
  Value: TUaVariant;
  DataType: TUaNodeId;
  ValueStr: string;
  VType: TVarType;
begin
  ValueStr := InputBox('Write', 'Write to ' + SelectedNode.Text, '');
  if ValueStr <> '' then
  begin
    DataType := UaClient.ReadAttribute(SelectedNode.NodeId, aiDataType).Value;
    try
      // Relying on Variant type conversions
      VType := UaClient.GetVarType(DataType);
      Value := TUaVariant.Create(VarAsType(ValueStr, VType));
      UaClient.WriteValue(SelectedNode.NodeId, Value);
    except
      on E: EVariantError do
      begin
        MessageDlg('Cannot convert ''' + ValueStr + ''' to ' + VarTypeToString(VType), mtError, [mbOK], 0);
      end;
    end;
  end;
end;
Writing structure types

You can write structure values to the server, when the variable data type so defines. Similar to [Reading structure values], you have two options: 1. for standard types, you can use the classes that are in the SDK 2. for custom types you can use IUaDynamicStructure.

To create a new standard structure value, you must use the respective Builder, which is available from each type. For example:

var
  ServerStatus: IUaServerStatusDataType;
  ServerStatusBuilder: TUaServerStatusDataType.IBuilder;
begin
  ServerStatusBuilder := TUaServerStatusDataType.Builder;
  ServerStatus := ServerStatusBuilder.StartTime(UTCNow).State(ssRunning).Build;
  Client.WriteValue(Id_Server_ServerStatus, ServerStatus);
...

Note that ServerStatus is not writable in servers, so the code above will raise an EUaStatusException.

To create a custom structure value, you can use the DynamicStructures and the respective builder.

procedure WriteDynamicStructureValue(Client: TUaClient; VariableId: TUaNodeId);
var
  DynamicStructureBuilder: TUaDynamicStructure.IBuilder;
  DynamicStructureTypeId: TUaNodeId;
  DynamicStructure: TUaVariant;
  S: string;
begin
  // Find the datatype of VariableId, and assume it is a structure type
  DynamicStructureTypeId := Client.ReadAttribute(VariableId, aiDataType).Value.AsNodeId;
  DynamicStructureBuilder := Client.TypeDictionary.CreateDynamicStructureBuilder(DynamicStructureTypeId);
  S := 'a string value'; // Assuming Field1 is of string type.
  DynamicStructureBuilder.SetFieldValue('Field1', S);
  DynamicStructure := DynamicStructureBuilder.Build;
  Client.WriteValue(VariableId, DynamicStructure);
...

10. Exceptions When Operations Fail

If any service call or operation fails, you will get an EUaException. When the complete service call fails, in case of a communication error, for example, you can expect a EUaServiceException. if you perform just a single operation, such as ReadValue or WriteValue, you may also get an EUaOperationException in case the server rejects the operation, due to an invalid NodeId or access restrictions, for example.

If you perform several operations inside a single call (such as ReadValues), you can still expect an EUaServiceException, but if individual requests are rejected, you will have to examine the StatusCodes that are provided as a response. Use TUaStatusCode.IsBad and .IsGood to check whether the operation failed or not.

StatusCode defines actually three different severities: Good, Bad and Uncertain. Good usually comes without additional information, whereas Bad and Uncertain will also include a more detailed error code. The status codes defined in the OPC UA specification are available in the UaBase unit and they begin with Bad_, Uncertain_ or Good_. The actual status code values are Cardinal values. So you can compare the values to known status codes, for example, with E.StatusCode = Bad_UserAccessDenied.

EUaServiceException and EUaOperationException also provide the respective StatusCode of the error in question.

11. Subscribe to Data Changes

In order to monitor data changes in the server, you need to define subscriptions. They are defined using the interface IUaSubscription and implemented by the class TUaSubscription These include a number of monitored items, which you listen to. To monitor data changes, you use the IUaMonitoredDataItem interface to declare them and TUaMonitoredDataItem class to implement them. For example:

Subscription := TUaSubscription.Create;
MonitoredDataItem := TUaMonitoredDataItem.Create(NodeId, aiValue, mmReporting, 0);
Subscription.AddItem(Item);
UaClient.AddSubscription(Subscription);

This defines a subscription with a single item. In principle, you can define any number of subscriptions and monitored items, although the server may impose limits on these. A big number of items may also affect the performance of the server in practice.

The subscription defines the default monitoring properties for all its items, but the items may also define individual sampling intervals (see the fields of TUaMonitoredDataItem) with the SamplingInterval property.

The subscription and monitored item parameters will define the frequency of the expected changes that server should provide.

In the client, you can listen to the change notifications with:

Item.OnDataChange := OnDataChange;

where

procedure OnDataChange(ASender: TObject; APrevValue, ANewValue: TUaDataValue);

Sender will be the monitored data item itself.

Alternatively, you can listen to the same data changes from the subscription.

You may also wish to listen to the alive and timeout events of the subscription. These will help you to 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 listeners to a subscription:

Subscription.OnAlive := MyAliveEvent;
Subscription.OnTimeout := MyTimeoutEvent;

, where MyAliveEvent and MyTimeoutEvent TNotifyEvent procedures.

12. Subscribe to Events

In addition to subscribing to data changes in the server Variables, you may also listen to events from event notifiers. You can use the same subscriptions, but instead of IUaMonitoredDataItem you use IUaMonitoredEventItem. You define an event listener, which gets notified when new events are received from the server. Additionally, you must also define the event filter (using the IUaEventFilter interface and the TUaEventFilter class, which define the event fields you wish to monitor (with the SelectClauses property of the IUaEventFilter interface) and optionally also which events you wish to receive (with the WhereClause property of the IUaEventFilter interface).

The fields and the event filter are defined in the sample client as follows:

DefaultBrowsePathStrings: TArray<String> = ['Message', 'EventId', 'Severity', 'Time', 'EventType', 'SourceNode'];
AttributeOperands := TList<IUaSimpleAttributeOperand>.Create;
for Path in DefaultBrowsePathStrings do begin
  BrowsePath := TList<TUaQualifiedName>.Create;
  BrowsePath.Add(TUaQualifiedName.Create(Path));
  AttributeOperands.Add(TUaSimpleAttributeOperand.Builder
    .BrowsePath(BrowsePath)
    .TypeDefinitionId(Id_BaseEventType)
    .AttributeId(Ord(TUaAttributeId.aiValue))
    .Build
  );
end;
EventFilter := TUaEventFilter.Builder.SelectClauses(AttributeOperands).Build;

Now we are ready to create the event item using the filter that was just created and the NodeId of the node that should be listened to:

MonitoredEventItem := TUaMonitoredEventItem.Create(TreeNode.NodeId, EventFilter);
Subscription.AddItem(MonitoredEventItem);
MonitoredEventItem.OnNewEvent := OnNewEvent;

The event listener callback OnNewEvent is defined as follows, and used to react to the event notification:

TUaEventNotificationEvent = procedure (Sender: TObject; EventFieldList: IUaEventFieldList) of object;

where Sender is the TUaMonitoredEventItem object.

13. History Access

OPC UA servers may also provide history information for the Nodes, including historical time series data and events. You can read the AccessLevel and UserAccessLevel attributes of a Variable Node to see whether history is available (if yes, then AccessLevel.HistoryRead should be included).

13.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).

To make your life a bit easier, UaClient also defines several convenience methods to make specific history requests. For example, the above can also be performed with

DataValue[] result = client.historyReadRaw(nodeId,
        DateTime.MIN_VALUE, DateTime.currentTime(),
        UnsignedInteger.MAX_VALUE, true, null, TimestampsToReturn.Source);

14. Call Methods

OPC UA also defines a mechanism to call Methods in the server Objects.

To call a Method, use TUaClient.Call:

OutputArguments := UaClient.Call(ObjectId, MethodId, InputArguments);

The ObjectId and MethodId parameters are the NodeIds of the Object that the method is part of and the method, respectively. You also need to provide the input arguments as an array of Variants (an empty array if the method has no input arguments), and the function will provide the output arguments of the method call as an array of Variants. You can also call the Call function by providing a CallMethodRequest.

Note that similar to Write Values, you will also need to ensure that the InputArguments are provided in correct data types, as excepted by the server. 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 UaCallMethodDlgs.pas) for more details.