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++ . The C++ client is very minimal, but there are also Tutorial projects for both Delphi and C++ installed to your 'Documents\ProsysOPC\Sentrol' folder. This tutorial will refer mostly to the LessonUa1_UaClient and LessonUa1_UaClient_BCB projects, when it explains the main tasks of an OPC UA client. So, please check out those applications to get a good start up to your client development.

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 TLessonUa1MainForm or TLessonUa1CMainForm). You can also create the component at run time as follows:

(Delphi)

var
  UaClient: TUaClient;
begin
  UaClient := TUaClient.Create(Owner);

(C++)

  TUaClient *UaClient = new TUaClient(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 TUaClient component.

The OPC UA SDK defines types as records, classes and interfaces. The usage in Delphi is rather straight forward, but in C++ they require a bit of extra attention.

Classes are used via pointers, such as TUaClient *UaClient above.

Records are used via reference, such as TUaLocalizedText AppName below.

Interfaces are used via Delphi interface references, such as _di_IUaSubscription Subscription in Subscribe to Data Changes. _di_IX types are actually typedefs that equal to DelphiInterface<IX>. DelphiInterface is a template wrapper that adds some functionality over the normal interface pointer. So, sometimes, you may still see them (in event arguments, for example) as interface pointers, IX *X.

The classes may define _di_IX operators to get the interface out of the objects, as is shown in the subcription example.

Subscription = S->operator _di_IUaSubscription();

The global Supports and IIinterface.Supports() are also useful for converting between the interface types.

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. The ApplicationDescription must be initialized in code as follows:

procedure TLessonUa1MainForm.InitClient;
begin
  ...
  UaClient.ApplicationIdentity.ApplicationDescription := TUaApplicationDescription.Builder
    .ApplicationUri('urn:localhost:UA:DelphiSampleClient')
    .ProductUri('urn:prosysopc.com:UA:DelphiSampleClient')
    .ApplicationName('DelphiSampleClient@localhost')
    .ApplicationType(atClient).Build;
void TLessonUa1CMainForm::InitClient()
{
  ..
  TUaLocalizedText AppName = TUaLocalizedText::Create("CBuilderSampleClient@localhost");
  UaClient->ApplicationIdentity->ApplicationDescription = TUaApplicationDescription::Builder
    ->ApplicationUri("urn:localhost:UA:CBuilderSampleClient")
        ->ProductUri("urn:prosysopc.com:UA:CBuilderSampleClient")
        ->ApplicationName(AppName)
        ->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);
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, in the CertificateStore (see below). 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 certificate 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, Global 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.

3.2.1. CertificateStore

To define which applications are allowed to have a connection, they use a Certificate Store, where the known Application Instance Certificates and Issuer Certificates are stored. The Certificate Store is in practice a folder on disk. The location is defined by the RootFolder property, which is by default 'PKI' - relative to the application executable. In many cases, it is better to change it to an abcolute path where the application can write. So, you can use the environment variables here, such as %ALLUSERSPROFILE% or %USERPROFILE%:

UaClient.CertificateStore.RootFolder := '%ALLUSERSPROFILE%\YourCompany\YourApplication\PKI';
UaClient->CertificateStore->RootFolder = "%ALLUSERSPROFILE%\YourCompany\YourApplication\PKI";

There are also properties for configuring the locations of the trusted, rejected and revoked certificates. Usually, you can just leave them as they are. The application’s own keys (that were explained above) are stored in the private folder. For this reason, make sure that you define the folders before calling LoadOrCreateCertificate for the ApplicationIdentity.

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 Issuer (a.k.a Certificate Authority (CA)), can be trusted automatically. To enable this, copy the Issuer certificate to the certs folder manually.

3.2.2. OnValidate

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. Usually, it is defined with the Object Inspector, but you can also define it at runtime:

UaClient.OnValidate := ValidateCertificate;
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 or not.

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

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.URI := 'opc.tcp://localhost:62620';
UaClient->ServerAddress->URI = "opc.tcp://localhost:62620"

Some sample addresses that you can use 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

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.

<hostname> is the hostname of the computer in which the server is running. If the server is running in the same computer, you can use 'localhost'. Or you can always use the IP address, as well. When converting the hostnames, the client will prefer IPV4 addresses, but IPV6 is supported as well, in general.

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.

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

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

UaClient.SecurityPolicies := AllSecurityPolicies104;
UaClient->SecurityPolicies = AllSecurityPolicies104;

SecurityPolicyStrategy will help to define the order of preference, when choosing the actual policy. It enables the client to support the best available policy for each server without any further modifications. The default value spsStrongSafe will prefer strong algorithms (256 bits), but avoids the deprecated policies. If you wish to prefer lighter security, you can change to spsFastSafe, for example.

The detailed definitions are as follows:

const
  HiSecurityPolicy = Ord(High(TUaSecurityPolicy));
  StrongSafePolicies: array [0..HiSecurityPolicy] of TUaSecurityPolicy = (
    spAes256Sha256RsaPss,
    spBasic256Sha256,
    spAes128Sha256RsaOaep,
    spBasic256,
    spBasic128Rsa15);
  StrongPolicies: array [0..HiSecurityPolicy] of TUaSecurityPolicy = (
    spAes256Sha256RsaPss,
    spBasic256Sha256,
    spBasic256,
    spAes128Sha256RsaOaep,
    spBasic128Rsa15);
  FastPolicies: array [0..HiSecurityPolicy] of TUaSecurityPolicy = (
    spAes128Sha256RsaOaep,
    spBasic128Rsa15,
    spBasic256Sha256,
    spAes256Sha256RsaPss,
    spBasic256);
  FastSafePolicies: array [0..HiSecurityPolicy] of TUaSecurityPolicy = (
    spAes128Sha256RsaOaep,
    spBasic256Sha256,
    spAes256Sha256RsaPss,
    spBasic256,
    spBasic128Rsa15);

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');
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;
UaClient->Connect();

If that fails, you will get an EUaConnectException.

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;
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 service calls are also monitored for timeouts, which means that there is no response from the server during that period. The default is 10000 (milliseconds), but you can define your own, if your server needs more processing time for some calls:

UaClient.OperationTimeout := 30000;
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 mandatory part of the OPC UA server address space. It will perform a check every StatusCheckInterval, which is 10000 milliseconds 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 TLessonUa1MainForm.UaClientServerStateChange(Sender: TObject;
  OldState, NewState: TUaServerState);
begin
  Log(Format('Server state changed from %s to %s',
    [UaServerStateToString(OldState),UaServerStateToString(NewState)]));
end;
void __fastcall TLessonUa1CMainForm::UaClientServerStateChange(TObject *Sender, TUaServerState OldState,
          TUaServerState NewState)
{
  TVarRec args[2] = {UaServerStateToString(OldState),UaServerStateToString(NewState)};
  Log(Sysutils::Format("Server state changed from %s to %s", args, 1));

}

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 Browser or the Delphi 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.

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');
TUaNamespace 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);
int 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');
TUaNamespace Namespace = TUaNamespace::Get("http://www.prosysopc.com/UA/Delphi/SampleNamespace");
TUaNodeId NodeId = TUaNodeId(Namespace, "MyLevel");

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

If you omit the Namespace, OPC UA Standard Namespace will be used.

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 one of

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:

Str := UaClient.NamespaceTable.NodeIdToString(NodeId, True);
String Str = UaClient->NamespaceTable->NodeIdToString(NodeId, True);

which gives

'ns=2;s=MyLevel'

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

ParseNodeId

If you get a NodeId in the above mentioned string format, you can parse it back to a TUaNodeId with

NodeId := UaClient.NamespaceTable.ParseNodeId(Str);
TUaNodeId NodeId = UaClient->NamespaceTable->ParseNodeId(Str);

It will convert from both NamespaceIndex ('ns=') and NamespaceUri ('nsu=') 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');
TUaNamespace Namespace = TUaNamespace::Get("http://www.prosysopc.com/UA/Delphi/SampleNamespace");
TUaQualifiedName QualifiedName = TUaQualifiedName(Namespace, "MyLevel");

If you omit the Namespace, the QualifiedName will be defined in the OPC UA Standard Namespace. This is correct, when you refer to standard properties, for example, but for custom properties, you should ensure that you use the proper 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:

Str := UaClient.NamespaceTable.UaQualifiedNameToString(QualifiedName);
String 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);
TUaQualifiedName 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. Now, if you call

References := UaClient.AddressSpace.Browse(NodeId);
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.

BrowseResultsListBox.Clear;
for I := 0 to Length(References)-1 do
  BrowseResultsListBox.Items.Add(References[i].DisplayName.Text);
BrowseResultsListBox->Clear();
for (int i = 0; i < References.Length; i++)
  BrowseResultsListBox->Items->Add(References[i]->DisplayName.Text);

See also TLessonUa1MainForm.BrowseResultsListBoxClick for more details about the References.

7.1. Browse parameters

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

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

DataValue := UaClient.ReadValue(VariableId);
TUaDataValue DataValue = UaClient->ReadValue(VariableId);

This is what you probably need to do most often. But note that if you need values for several variables ar once, you should consider using ReadValues instead.

There is also a Read method, which 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 for changes, instead of just reading there values once, you had better use the Subscriptions, as described below.

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 two timestamps: one called SourceTimestamp and one called ServerTimestamp. The timestamps come with extra fields for picosecond level of accuracy. SourceTimestamp is supposed to reflect the actual change time in the data source, whereas ServerTimestamp reflects the time when the OPC UA server was last checking the value.

8.1.1. Value as Variant

OPC UA uses a 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 (Also in C++ you have to use the TUaVariant::Create methods, because the implicit conversions are not supported by the compiler) . 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;
...
TUaDataValue DataValue = UaClient->ReadValue(Id_Server_ServerStatus);
TUaStatusCode StatusCode = DataValue.StatusCode;

if (StatusCode.IsGood) {
  _di_IUaStructure S = DataValue.Value.AsStructure;
  _di_IUaServerStatusDataType ServerStatus;
  if (S->Supports(ServerStatus)) {
    ...

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.

procedure TLessonUa1MainForm.ReadButtonClick(Sender: TObject);
var
  DataValue: TUaDataValue;
  VariableId: TUaNodeId;
  DynamicStructureTypeId: TUaNodeId;
  DynamicStructureValue: IUaDynamicStructure;
  FieldValue: TUaVariant;
  FieldName: string;
begin
  VariableId := GetNodeId();

  //Read
  Log('Read ' + VariableId.ToString());
  try
    DataValue := UaClient.ReadValue(VariableId);
    Log(DataValue.ToString());
    if DataValue.Value.BuiltinType = bitExtensionObject then
    begin
      // DataValue.ToString already printed out the complete value of the structure,
      // but we will show how to use IUaDynamicStructure to
      // print out the value of the first field, as an example
      DynamicStructureValue := DataValue.Value.AsStructure as IUaDynamicStructure;
      FieldName := DynamicStructureValue.Specification.Fields[0].Name;
      FieldValue := DynamicStructureValue.FieldValues[FieldName];
      Log(FieldName + ': ' + FieldValue.ToString()); // Must use ToString!
    end;
  except
    on E: Exception do
      Log(E.Message);
  end;
end;
void __fastcall TLessonUa1CMainForm::ReadButtonClick(TObject *Sender)
{
  TUaNodeId VariableId = GetNodeId();

  //Read
  Log("Read " + VariableId.ToString());
  try {
    TUaDataValue DataValue = UaClient->ReadValue(VariableId);
    Log(DataValue.ToString());
    if (DataValue.Value.BuiltinType == bitExtensionObject)
    {
      // DataValue.ToString already printed out the complete value of the structure,
      // but we will show how to use IUaDynamicStructure to
      // print out the value of the first field, as an example
      _di_IUaStructure S = DataValue.Value.AsStructure;
      // Convert to _di_IUaDynamicStructure using Supports
      _di_IUaDynamicStructure DynamicStructureValue;
      if (S->Supports(DynamicStructureValue))
      {
        String FieldName = DynamicStructureValue->Specification.Fields[0].Name;
        TUaVariant FieldValue = DynamicStructureValue->FieldValues[FieldName];
        Log(FieldName + ": " + FieldValue.ToString()); // Must use ToString!
      }
    }
  } catch (Exception *E) {
    Log(E->Message);
  }

}

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.WriteValue(NodeId, Value);
boolean Status := UaClient->WriteValue(NodeId, 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 WriteValue, you can call WriteAttribute to write to any attribute of any node. Not all attributes accept writes, though, and 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:

var
  DataType: TUaNodeId;
  Value: TUaVariant;
  ValueStr: string;
  VariableId: TUaNodeId;
  VType: TVarType;
begin
  ValueStr := ValueEdit.Text;

  // Find out the data type of the variable
  DataType := UaClient.ReadAttribute(VariableId, aiDataType).Value.AsNodeId;
  if DataType.IsNull() then
  begin
    ShowMessage('Not a variable: cannot write value');
    Exit;
  end;

  // Convert to the expected data type using Variant type conversions
  VType := UaClient.GetVarType(DataType);
  try
    Value := TUaVariant.Create(VarAsType(ValueStr, VType));
  except
    on E: EVariantError do
    begin
      MessageDlg('Cannot convert ' + ValueStr + ' to ' + VarTypeToString(VType), mtError, [mbOK], 0);
      Exit;
    end;
  end;

  UaClient.WriteValue(VariableId, Value);
String ValueStr = ValueEdit->Text;

// Find out the data type of the variable
TUaNodeId DataType = UaClient->ReadAttribute(VariableId, aiDataType).Value.AsNodeId;
if (DataType.IsNull()) {
  ShowMessage("Not a variable: cannot write value");
  return;
}

// Convert to the expected data type using Variant type conversions
TVarType VType = UaClient->GetVarType(DataType);
TUaVariant Value;
try {
  Value = TUaVariant::Create(VarAsType(ValueStr, VType));
} catch (EVariantError *E) {
  MessageDlg("Cannot convert '" + ValueStr + "' to " + VarTypeToString(VType), mtError, mbOKOnly, 0);
  return;
}
UaClient->WriteValue(VariableId, Value));

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

procedure WriteServerStatus(Client: TUaClient);
var
  ServerStatus: IUaServerStatusDataType;
  ServerStatusBuilder: TUaServerStatusDataType.IBuilder;
begin
  ServerStatusBuilder := TUaServerStatusDataType.Builder;
  ServerStatus := ServerStatusBuilder.StartTime(UTCNow).State(ssRunning).Build;
  Client.WriteValue(Id_Server_ServerStatus, ServerStatus);
end;
...
void WriteServerStatus(TUaClient *Client)
{
  TUaServerStatusDataType::IBuilder *ServerStatusBuilder = TUaServerStatusDataType::Builder;
  IUaServerStatusDataType *ServerStatus = ServerStatusBuilder->
    StartTime(UTCNow())->State(ssRunning)->Build();
  TUaVariant Value = TUaVariant::Create(ServerStatus);
  Client->WriteValue(Id_Server_ServerStatus, Value);
}

Note that ServerStatus is not writable in servers, so the code above will raise an EUaStatusException. The example is just given with ServerStatus because it is structure type that exists in every server.

Servers can also define their own custom structure types or they can use structure types defined in specific information models. In both cases, the server exposes information about the types through a so called DataDictionary. The DataDictionary enables the client to interpret these types without any prior information about them. However, this information is only available at runtime, so you will need to work with a special type that the SDK provides for that. We call it DynamicStructure, which can handle any structure value available from the server.

To construct a DynamicStructure, you need to use the DynamicStructureBuilder.

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);
...
void WriteDynamicStructureValue(TUaClient *Client, TUaNodeId VariableId)
{
  // Find the datatype of VariableId, and assume it is a structure type
  TUaNodeId DynamicStructureTypeId = Client->ReadAttribute(VariableId, aiDataType).Value.AsNodeId;
  TUaDynamicStructure::IBuilder *DynamicStructureBuilder =
          Client->TypeDictionary->CreateDynamicStructureBuilder(DynamicStructureTypeId);
  String S = "a string value"; // Assuming Field1 is of string type.
  DynamicStructureBuilder->SetFieldValue("Field1", TUaVariant::Create(S));
  _di_IUaStructure Structure = DynamicStructureBuilder->Build();
  TUaVariant Value = TUaVariant::Create(Structure);
  Client->WriteValue(VariableId, Value);
}

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:

var
  Subscription: IUaSubscription;
  MonitoredDataItem: IUaMonitoredDataItem;
...
Subscription := TUaSubscription.Create;
UaClient.AddSubscription(Subscription);

MonitoredDataItem := TUaMonitoredDataItem.Create(VariableId);
Subscription.AddItem(MonitoredDataItem);
TUaSubscription *S = new TUaSubscription();
_di_IUaSubscription Subscription = S->operator _di_IUaSubscription();
UaClient->AddSubscription(Subscription);

TUaMonitoredDataItem *M = new TUaMonitoredDataItem(VariableId);
_di_IUaMonitoredDataItem MonitoredDataItem = M->operator _di_IUaMonitoredDataItem();
Subscription->AddItem(MonitoredDataItem);

where VariableId is the NodeId of a Variable to monitor.

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 the server should provide.

In the example, the subscription is created first and added to the client, before the monitored item is created and added to the subscription.

If you wish to use several monitored items, it is recommended to create and add them all to the subscription before adding the subscription to the client. This will create all monitored items to the server with one service call, which is obviously much more efficient.

For example:

Subscription := TUaSubscription.Create;

for Index := 0 to Length(VariableIds)-1 do
begin
  MonitoredDataItem := TUaMonitoredDataItem.Create(VariableIds[Index]);
  Subscription.AddItem(MonitoredDataItem);
end;
UaClient.AddSubscription(Subscription);
TUaSubscription *S = new TUaSubscription();
_di_IUaSubscription Subscription = S->operator _di_IUaSubscription();

for (int i = 0; i < A.Length; i++) {
  TUaMonitoredDataItem *M = new TUaMonitoredDataItem(VariableId);
  _di_IUaMonitoredDataItem MonitoredDataItem = M->operator _di_IUaMonitoredDataItem();
  Subscription->AddItem(MonitoredDataItem);
}
UaClient->AddSubscription(Subscription);

11.1. OnDataChange

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

MonitoredDataItem.OnDataChange := OnDataChange;
MonitoredDataItem->OnDataChange = OnDataChange;

where for example

procedure TLessonUa1MainForm.OnDataChange(ASender: IUaMonitoredDataItem;
  APrevValue, ANewValue: TUaDataValue);
begin
  Log(ASender.NodeId.ToString() + ': ' + ANewValue.Value.ToString());
end;
void __fastcall TLessonUa1CMainForm::OnDataChange(_di_IUaMonitoredDataItem ASender, const TUaDataValue &APrevValue,
        const TUaDataValue &ANewValue)
{
  String NodeIdString = ASender->NodeId.ToString();
  String ValueString = const_cast<TUaVariant&>(ANewValue.Value).ToString();
  Log(NodeIdString + ": " + ValueString);
}

Sender will be the monitored data item itself.

Alternatively, you can listen to the data changes from the subscription using Subsciption.OnDataChange.

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;
Subscription->OnAlive = MyAliveEvent;
Subscription->OnTimeout = MyTimeoutEvent;

, where MyAliveEvent and MyTimeoutEvent are normal 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:

procedure TLessonUa1MainForm.CreateMonitoredEventItem(NodeId: TUaNodeId);
var
  EventFilter: IUaEventFilter;
  Path: String;
  BrowsePath: TArray<TUaQualifiedName>;
  MonitoredEventItem: IUaMonitoredEventItem;
  AttributeOperands: TList<IUaSimpleAttributeOperand>;

  // The fields to monitor
  function DefaultBrowsePathStrings: TArray<String>;
  begin
    SetLength(Result, 7);
    Result[0] := 'Message';
    Result[1] := 'EventId';
    Result[2] := 'Severity';
    Result[3] := 'Time';
    Result[4] := 'EventType';
    Result[5] := 'SourceNode';
    Result[6] := 'SourceName';
  end;
begin
  // Convert the fields to AttributeOperands
  AttributeOperands := TList<IUaSimpleAttributeOperand>.Create;
  try
    for Path in DefaultBrowsePathStrings do
    begin
      BrowsePath := UaClient.NamespaceTable.ParseQualifiedNameArray(Path);
      AttributeOperands.Add(TUaSimpleAttributeOperand.Builder
        .BrowsePath(BrowsePath)
        .TypeDefinitionId(Id_BaseEventType)
        .AttributeId(aiValue)
        .Build);
    end;
    EventFilter := TUaEventFilter.Builder
      .SelectClauses(AttributeOperands.ToArray)
    .Build;
  finally
    AttributeOperands.Free;
  end;

  // Create the monitored Item
  MonitoredEventItem := TUaMonitoredEventItem.Create(NodeId, EventFilter);
  Subscription.AddItem(MonitoredEventItem);
  MonitoredEventItem.OnNewEvent := OnNewEvent;
end;
void TLessonUa1CMainForm::CreateMonitoredEventItem(TUaNodeId NodeId)
{
  // Monitored Event Item

  // The fields to monitor
  DynamicArray<String> DefaultBrowsePathStrings;
  DefaultBrowsePathStrings.Length = 7;
  DefaultBrowsePathStrings[0] = "Message";
  DefaultBrowsePathStrings[1] = "EventId";
  DefaultBrowsePathStrings[2] = "Severity";
  DefaultBrowsePathStrings[3] = "Time";
  DefaultBrowsePathStrings[4] = "EventType";
  DefaultBrowsePathStrings[5] = "SourceNode";
  DefaultBrowsePathStrings[6] = "SourceName";

  // Convert the fields to AttributeOperands
  DynamicArray<_di_IUaSimpleAttributeOperand> AttributeOperands;
  AttributeOperands.Length = DefaultBrowsePathStrings.Length;
  for (int i=0; i<AttributeOperands.Length; i++)
  {
    DynamicArray<TUaQualifiedName> BrowsePath =
      UaClient->NamespaceTable->ParseQualifiedNameArray(DefaultBrowsePathStrings[i]);
    AttributeOperands[i] = TUaSimpleAttributeOperand::Builder
      ->BrowsePath(BrowsePath)
      ->TypeDefinitionId(Id_BaseEventType)
      ->AttributeId(aiValue)
      ->Build();
  }
  _di_IUaEventFilter EventFilter = TUaEventFilter::Builder
    ->SelectClauses(AttributeOperands)
    ->Build();

  // Create the monitored Item - monitor all events from the Server object
  TUaMonitoredEventItem *M = new TUaMonitoredEventItem(NodeId, EventFilter);
  _di_IUaMonitoredEventItem MonitoredEventItem = M->operator _di_IUaMonitoredEventItem();
  Subscription->AddItem(MonitoredEventItem);
  MonitoredEventItem->OnNewEvent = OnNewEvent;

  Log("Created MonitoredEventItem to " + MonitoredEventItem->NodeId.ToString(true /*IncludeAlias*/));
}

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);
TUaMonitoredEventItem *M = new TUaMonitoredEventItem(NodeId, EventFilter);
_di_IUaMonitoredEventItem MonitoredEventItem = M->operator _di_IUaMonitoredEventItem();
Subscription->AddItem(MonitoredEventItem);

12.1. OnNewEvent

You can listen to notifications of new events with:

MonitoredEventItem.OnNewEvent := OnNewEvent;
MonitoredEventItem->OnNewEvent = OnNewEvent;
procedure TLessonUa1MainForm.OnNewEvent(Sender: IUaMonitoredEventItem;
  EventFieldList: IUaEventFieldList);
var
  I: Integer;
  Name: TUaLocalizedText;
  NodeId: TUaNodeId;
  Path: string;
  S: TStringList;
  V: TUaVariant;
  VS: string;
begin
  S := TStringList.Create;
  try
    for I := 0 to EventFieldList.EventFieldCount-1 do
    begin
      Path := UaBrowsePathToString(Sender.EventFilter.SelectClauses[I]);
      V := EventFieldList.EventFields[I];
      if V.BuiltinType = bitNodeId then
      begin
        // The field value is a NodeId - let's find the DisplayName of the respective node instead
        NodeId := V.AsNodeId;
        Name := UaClient.ReadAttribute(NodeId, aiDisplayName).Value.AsLocalizedText;
        VS := Name.Text;
      end
      else
        VS := V.ToString();
      S.Values[Path] := VS;
    end;
    Log(S.CommaText);
  finally
    S.Free;
  end;
end;
void __fastcall TLessonUa1CMainForm::OnNewEvent(_di_IUaMonitoredEventItem Sender, Prosysopc::Uabase::_di_IUaEventFieldList EventFieldList)
{
  String s = "New Event: ";
  for (int i = 0; i < EventFieldList->EventFieldCount; i++) {
    TUaVariant V = EventFieldList->EventFields[i];
    String path = UaBrowsePathToString(Sender->EventFilter->SelectClauses[i]);
    String vs;
    if (V.BuiltinType == bitNodeId) {
      // The field value is a NodeId - let's find the DisplayName of the respective node instead
      TUaNodeId NodeId = V.AsNodeId;
      TUaLocalizedText Name = UaClient->ReadAttribute(NodeId, aiDisplayName).Value.AsLocalizedText;
      vs = Name.Text;
    }
    else
      vs = V.ToString();
    s = s + path + "=" + vs + " ";
  }
  Log(s);
}

where Sender is the TUaMonitoredEventItem object.

13. Call Methods

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

Let’s first initialize the InputArguments and OutputArguments properties. You can use these to find details about the argument names, data types, descriptions, etc. These are defined as properties of the method (of the Object or ObjectType) that you wish to call. The following method reads them from the server and converts to arrays of IUaArgument.

procedure TLessonUa1MainForm.GetArguments(MethodId: TUaNodeId; var
  InputArguments, OutputArguments: TArray<IUaArgument>);
var
  Refs: TArray<IUaReferenceDescription>;
  I: Integer;
  J: Integer;
  V: TUaVariant;
begin
  Refs := UaClient.AddressSpace.Browse(MethodId);
  for I := 0 to Length(Refs)-1 do
  begin
    if (Refs[i].BrowseName.Name = 'InputArguments') then
    begin
      V := UaClient.ReadValue(Refs[i].NodeId).Value;
      SetLength(InputArguments, V.ArrayLength);
      for J := 0 to V.ArrayLength-1 do
        InputArguments[J] := V[J].AsStructure as IUaArgument;
    end
    else if (Refs[i].BrowseName.Name = 'OutputArguments') then
    begin
      V := UaClient.ReadValue(Refs[i].NodeId).Value;
      SetLength(OutputArguments, V.ArrayLength);
      for J := 0 to V.ArrayLength-1 do
        OutputArguments[J] := V[J].AsStructure as IUaArgument;
    end;
  end;
end;
void TLessonUa1CMainForm::GetArguments(TUaNodeId MethodId,
  DynamicArray<_di_IUaArgument> InputArguments,
  DynamicArray<_di_IUaArgument> OutputArguments)
{
  // Prompt the InputArguments from the user, one by one
  DynamicArray<_di_IUaReferenceDescription> Refs = UaClient->AddressSpace->Browse(MethodId);
  for (int i = 0; i < Refs.Length; i++) {
    if (Refs[i]->BrowseName.Name == "InputArguments") {
      TUaVariant V = UaClient->ReadValue(Refs[i]->NodeId).Value;
      InputArguments.Length = V.ArrayLength;
      for (int j = 0; j < V.ArrayLength; j++) {
        _di_IUaStructure S = V[j].AsStructure;
        // Convert to _di_IUaArgument using Supports
        if (!S->Supports(InputArguments[j]))
          throw new Exception("InputArgument[" + IntToStr(j) + " is not an Argument");
      }
    } else if (Refs[i]->BrowseName.Name == "OutputArguments") {
      TUaVariant V = UaClient->ReadValue(Refs[i]->NodeId).Value;
      OutputArguments.Length = V.ArrayLength;
      for (int j = 0; j < V.ArrayLength; j++) {
        _di_IUaStructure S = V[j].AsStructure;
        // Convert to _di_IUaArgument using Supports
        if (!S->Supports(OutputArguments[j]))
          throw new Exception("OutputArgument[" + IntToStr(j) + " is not an Argument");
      }
    }
  }
}

Next, you will have to

To call a Method, use TUaClient.Call:

OutputArgs := UaClient.Call(ObjectId, MethodId, InputArgs);
DynamicArray<TUaVariant> OutputArgs = UaClient->Call(ObjectId, MethodId, InputArgs);

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.

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_TypeMismatch errors for each argument that is not provided in the correct data type. See the PromptInputs method in the Tutorial project (Delphi & C++) and the UaSampleClient (in UaCallMethodDlgs.pas) for more details on how to compose the argument values.

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

14.1. Reading History

To actually read history data, you have several options. The basic method is to read raw samples from the servers historical records using HistoryReadRaw:

procedure TLessonUa1MainForm.HistoryReadButtonClick(Sender: TObject);
var
  EndTime: TDateTime;
  I: Integer;
  StartTime: TDateTime;
  Values: TArray<TUaDataValue>;
  VariableId: TUaNodeId;
begin
  VariableId := GetNodeId();
  EndTime := UTCNow;
  StartTime := EndTime - OneMinute;

  Log('Reading history for ' + VariableId.ToString + ' between ' +
    DateTimeToStr(StartTime) + ' and ' + DateTimeToStr(EndTime));
  Values := UaClient.HistoryReadRaw(VariableId, StartTime, EndTime);
  for I := 0 to High(Values) do
    Log(Values[I].ToString);
end;
void __fastcall TLessonUa1CMainForm::HistoryReadButtonClick(TObject *Sender)
{
  TUaNodeId VariableId = GetNodeId();
  TDateTime EndTime = UTCNow();
  TDateTime StartTime = (Double) EndTime - (Double) OneMinute;

  Log("Reading history for " + VariableId.ToString() + " between " +
    DateTimeToStr(StartTime) + " and " + DateTimeToStr(EndTime));
  DynamicArray<TUaDataValue> Values = UaClient->HistoryReadRaw(VariableId, StartTime, EndTime);
  for (int i = 0; i <= Values.High; i++) {
    Log(Values[i].ToString());
  }
}

You can also check other methods, such as HistoryReadAtTimes and HistoryReadProcessed. Note that all of these read a single history at a time. If you need several histories, it may be more efficient to use the HistoryRead, which is a bit more complex to use, though. It takes in HistoryReadDetails structures, which corresnpond to the parameteres given to the specific history read options, as above.