.NET Based OPC UA Client/Server SDK  3.4.2.542
Console Client

Overview

The Console Client is an extended example developed using the Unified Automation OPC UA Client SDK .NET showing several features of OPC UA.

This client is a console application that runs for all of our provided Frameworks (.NET Framework, .NET Core, .NET). The application is working with the DemoServers of UnifiedAutomation. You can edit a configuration file to be able to work with other servers.

The client shows a menu. By pressing a key an action will be triggered.

Connection

The connection menu is the entry point of the client. Here you can trigger some actions that need to be done before a connection is established and you can connect to the configured server.

Setting UserIdentity

With "set user to Anonymous" and "set username and password" you can set the identity token for the session. The identity must be set before Session.Connect is called. By default, the UserIdentity is null. In this case an anonymous user identity will be used to connect to the server. By selecting "set username and password", the username and password combination from the configuration file is used.

if (Session.UserIdentity == null)
{
Session.UserIdentity = new UserIdentity();
}
Session.UserIdentity.IdentityType = UserIdentityType.UserName;
Session.UserIdentity.UserName = Settings.Connection.UserName;
Session.UserIdentity.Password = Settings.Connection.Password;

Simple Connect

An option to connect to a server is to call Session.Connect(string discoveryUrl, SecuritySelection securitySelection). This method implicitly calls several OPC UA services. You can specify with the securitySelection parameter if you want to connect to the endpoint with the SecurityPolicy http://opcfoundation.org/UA/SecurityPolicy#None or to the endpoint with the best possible SecurityPolicy. The Client SDK makes sure that only endpoints with supported security policies get chosen. If you try to connect with a SecurityPolicy, while no endpoint with the selected policy exists on the server, an exception is thrown by the SDK.

Session.Connect(Settings.Connection.DiscoveryUrl, security ? SecuritySelection.BestAvailable : SecuritySelection.None);

Discovery

A second option to connect to a server is to use the Discovery class to get the available endpoints of a server. Afterwards, one of these endpoints is selected to call Session.Connect.

using (Discovery discovery = new Discovery(Application))
{
Endpoints = discovery.GetEndpoints(discoveryUrl);
Output("\nGetEndpoints succeeded");
}
Session.Connect(SelectedEndpoint, null);

The GetEndpoints method call might return endpoints with a security policy that is not supported by the client. So, before selecting the endpoint the application should check if the CryptoProvider supports the security policy.

string unsupported;
try
{
Application.SecurityProvider.CreateCryptoProvider(
new CryptoProviderSettings()
{
SecurityProfileUri = Endpoints[i].SecurityPolicyUri
});
unsupported = "";
}
catch (Exception)
{
unsupported = " [Not supported by CryptoProvider]";
}

Reverse Connect

Another option to connect to a server is to let the server do the initial connection establishment by a Reverse Connect. This is useful if the server is behind a firewall and no port can be opened. In this case, the server has to be configured for Reverse Connect, too. On the client, the session has to be created differently, containing the clientListeningUrl.

try
{
m_session = new Session(Application, Settings.Connection.ClientUrlForReverseConnect);
}
catch (System.Net.Sockets.SocketException se)
{
LogException(se, "\nCreating Session failed");
Output($"Check if other application binds the port {Settings.Connection.ClientUrlForReverseConnect}");
throw;
}

The connect itself is equal to the Simple Connect

Session.ReverseConnect(Settings.Connection.DiscoveryUrl, SecuritySelection.None);

Security

Set SecurityProvider

To be able to create or manage the own application instance certificate, a SecurityProvider must be set to the ApplicationInstanceBase. The SDK includes two implementations of ISecurityProvider interface which support creating certificates. There is the BouncyCastleSecurityProvider which is implemented in the package UaBase.BouncyCastle. And there is the WindowsSecurityProvider which is implemented in the package UaBase.Windows. The WindowsSecurityProvider is only available for applications that run in a Windows environment. The class ApplicationInstance implemented in the package Uabase.Windows sets the WindowsSecurityProvider implicitly.

In this example the SecurityProvider is set depending on the used framework. Since the same source code files are used, the distinction is done with a compilation symbol.

#if NETFRAMEWORK
var securityProvider = new WindowsSecurityProvider();
#else
var securityProvider = new BouncyCastleSecurityProvider();
#endif
ApplicationInstanceBase.Default.SecurityProvider = securityProvider;

Trust Certificates

To establish a connection with security the client and the server application have to trust the certificate of the other application. The event handler ApplicationInstanceBase.UntrustedCertificate is called if a certificate is received that is not trusted yet.

If the property UntrustedCertificateEventArgs.Accept is set to true within the event handler, the certificate will be accepted. If the property UntrustedCertificateEventArgs.Persist is set to true within the event handler, the certificate will be stored in the offline trustlist, i.e., after a restart of the application the certificate is still trusted.

In this example, there is a user interaction to accept or reject the certificate.

The application blocks until the event handler returns.

ApplicationInstanceBase.Default.UntrustedCertificate += new UntrustedCertificateEventHandler(Application_UntrustedCertificate);
static void Application_UntrustedCertificate(object sender, UntrustedCertificateEventArgs e)
{
List<StatusCode> validationErrors = e.ValidationError.Flattened;
Console.WriteLine("\n");
Console.WriteLine("-------------------------------------------------------");
if (validationErrors.Count == 1)
{
if (e.ValidationError == StatusCodes.BadCertificateUntrusted)
{
Console.WriteLine($" - Untrusted certificate: {e.Certificate}");
Console.WriteLine("-------------------------------------------------------");
Console.WriteLine(" - Press 0 to accept the certificate -");
}
else
{
Console.WriteLine($" - Validation error in certificate: {e.ValidationError}, {e.Certificate}");
Console.WriteLine("-------------------------------------------------------");
Console.WriteLine(" - Press 0 to accept the validation error -");
}
Console.WriteLine(" - Press other key to reject the certificate -");
ConsoleKeyInfo key = Console.ReadKey();
switch (key.Key)
{
case ConsoleKey.D0:
case ConsoleKey.NumPad0:
e.Accept = true;
e.Persist = true;
break;
default:
break;
}
}
else
{
if (e.ValidationError == StatusCodes.BadCertificateUntrusted)
{
Console.WriteLine($" - Untrusted certificate: {e.Certificate}");
Console.WriteLine("-------------------------------------------------------");
}
else
{
Console.WriteLine($" - Validation error in certificate: {e.ValidationError}, {e.Certificate}");
Console.WriteLine("-------------------------------------------------------");
}
Console.WriteLine(" - Further validation errors:");
for (int ii = 1; ii < validationErrors.Count; ii++)
{
Console.WriteLine($" - {validationErrors[ii]}");
}
if (e.ValidationError == StatusCodes.BadCertificateUntrusted)
{
Console.WriteLine(" - Press 0 to trust the certificate -");
}
else
{
Console.WriteLine(" - Press 0 to accept this validation error -");
}
Console.WriteLine(" - Press 1 to accept all validation errors -");
Console.WriteLine(" - Press other key to reject the certificate -");
ConsoleKeyInfo key = Console.ReadKey();
switch (key.Key)
{
case ConsoleKey.D0:
case ConsoleKey.NumPad0:
e.Accept = true;
e.Persist = true;
break;
case ConsoleKey.D1:
case ConsoleKey.NumPad1:
e.AcceptAll = true;
e.Persist = true;
break;
default:
break;
}
}
}

GDS Configuration

Instead of trusting the certificates manually the Security Management can be done by a Global Discovery Server (GDS). This tutorial shows the usage of UaGDS (https://www.unified-automation.com/products/ua-runtime-software/uagds.html).

In this example the certificate management by the GDS can be enabled in the configuration.

<GdsSettings>
<UseGdsSecurity>true</UseGdsSecurity>
</GdsSettings>

The client application can register itself at the GDS and do the complete GDS handling itself using the class GdsPullManagement. An instance of this class shall be set at ApplicationInstanceBase.GdsHandler before the application has been started. Additionally the EndpointUrl of the GDS has to be set in the application settings of the server application.

var pullManagement = new GdsPullManagement(ApplicationInstanceBase.Default);
ApplicationInstanceBase.Default.GdsHandler = pullManagement;
<Extension>
<GdsSettings xmlns="http://unifiedautomation.com/schemas/2011/12/Application.xsd">
<GdsDiscoveryUrl>opc.tcp://localhost:48060</GdsDiscoveryUrl>
</GdsSettings>
</Extension>

In this example we are using a different file location for the certificate and the trust list managed by the GDS to be able to switch back to non GDS management.

ConfigurationInMemory clientConfigurationInMemory = configuration as ConfigurationInMemory;
clientConfigurationInMemory.SetSecurity(PlatformUtils.CombinePath(PlatformUtils.IsWindows() ? "%CommonApplicationData%" : "%LocalApplicationData%", "UnifiedAutomation", "UaSdkNetBundleBinary", gdsFolder), "CN=ConsoleClient/O=UnifiedAutomation/DC=localhost");

After the client has been started for the first time and the application instance certificate of the GDS has been accepted, the client application will be visible in the Pending Lists for applications to register and certificate signing requests in the GDS Configuration Tool. After accepting both, the client will receive a new certificate and trust list.

consoleclient_pendinglist.png
Accept application in pending list for application registration
consoleclient_pendinglist2.png
Accept application in pending list for certificate signing requests

Change user

OPC UA allows to change the user of a session while the client is connected. In the Client SDK, first the new user credentials need to be provided to the session object.

Session.UserIdentity.IdentityType = UserIdentityType.UserName;
Session.UserIdentity.UserName = Settings.Connection.UserName;
Session.UserIdentity.Password = Settings.Connection.Password;

Afterwards, a simple method needs to be called, which triggers the actual OPC UA service request.

Session.ChangeUser();

Change password

OPC UA User Management allows authenticated users, with IdentityType.UserName, to change there own password, event without security access rights.
For this case, the Client SDK provides an easy to use method to change the password. This method can be found at the session object.

Session.ChangePassword(oldPassword, newPassword);
Note
This method can only be used with connected and encrypted sessions.

Mapping namespaces

The client has a configuration that defines the NodeIds that the client uses to call the OPC UA services. The NamespaceIndex of the NodeIds cannot be configured directly because it may be different for different servers. So there is also a NamespaceTable configured which entries are referred by the NodeIds in the configuration.

After a connection is established you can access the current NamespaceTable of the server.

Settings.CurrentNamespaceTable = Session.NamespaceUris;

You can use this table to map the indices of the configuration to the indices in the server. This mapping can be done directly after the Connect call has finished.

m_currentMapping = value.CreateMapping(ConfiguredNamespaces, false);
NodeId MapNodeId(NodeId nodeId)
{
if (m_currentMapping == null)
{
throw new Exception("Configuration Error");
}
if (nodeId.NamespaceIndex > m_currentMapping.Length)
return nodeId;
return new NodeId(nodeId.IdType, nodeId.Identifier, m_currentMapping[nodeId.NamespaceIndex]);
}

In this example the configured namespaces of the configuration must be updated after the NodeIds are mapped.

ConfiguredNamespaces.Clear();
NamespaceTable tmp = new NamespaceTable();
List<ushort> usedNamespaces = new List<ushort>();
foreach (ushort index in m_currentMapping)
{
usedNamespaces.Add(index);
}
for (int ii = 1; ii < value.Count; ii++)
{
tmp.Add(usedNamespaces.Contains((ushort) ii) ? value[ii] : null);
}
ConfiguredNamespaces = tmp;

Session services

Most OPC UA services require a session to the server. The most important ones are described here. All methods that do an OPC UA service request have a synchronous and an asynchronous version in the SDK. In this example the asynchronous methods are only used for few service calls, since synchronous methods are easier to understand. On the other hand, the application will block until the synchronous methods return. Therefore, in real applications often the asynchronous methods should be used.

Read

Read Values

This example shows how to read the Value attribute of some configured nodes. The Read method uses a list of ReadValueIds as argument. This list is constructed with configured nodes.

List<ReadValueId> readValueIds = new List<ReadValueId>();
foreach (NodeId nodeId in configuredVariables)
{
readValueIds.Add(new ReadValueId()
{
AttributeId = Attributes.Value,
NodeId = nodeId
});
}
return readValueIds;

The Read method of the SDK is overloaded with different parameterization options. In this example we are using the shortest one for the synchronous call.

var results = Session.Read(nodesToRead);

The asynchronous method BeginRead takes four arguments. The first argument is the list of nodes to read. The second argument indicates the maximum age of the variables. It signals if the server can return a cached value or not. In most cases this argument should be 0. The third argument is the AsyncCallback and the forth argument is the user data that will be returned in the IAsycResult when the SDK calls the AsyncCallback.

Session.BeginRead(nodesToRead, 0, OnReadComplete, nodesToRead);

For the asynchronous call you need to implemented an AsyncCallback that is called by the SDK when the server returns the read results. This method must be used as argument for the asynchronous method call.

void OnReadComplete(IAsyncResult result)
{
try
{
List<DataValue> results = Session.EndRead(result);

Read an Array with Index

OPC UA does not only provide scalar values, but also arrays. You can read the complete array, or only parts of it. The example shows how to read only parts of an array by defining the IndexRange in the ReadValueId.

foreach (NodeId nodeId in Settings.ReadWithIndexRangeVariableIds)
{
nodesToRead.Add(new ReadValueId()
{
AttributeId = Attributes.Value,
NodeId = nodeId,
IndexRange = "1:3"
});
}

Write

Write Values

The Write method uses a list of WriteValues as argument. You need to set the NodeId, AttributeId and the Value of the WriteValueId for each node attribute that should be written. The client loads this information, including the value, from its configuration.

IList<WriteValue> nodesToWrite = Settings.WriteVariables;
List<StatusCode> res = Session.Write(nodesToWrite);

Write an Array with Index

Like reading, OPC UA also provides the capabilities to write parts of an array, by defining the IndexRange property of the WriteValue structure.

nodeIdsToWrite.Add(new WriteValue()
{
NodeId = new NodeId("Demo.Static.Arrays.Boolean", 2),
Value = new DataValue() { WrappedValue = new Variant(value) },
AttributeId = Attributes.Value,
IndexRange = "0:1"
});

Write a Structure

The previous examples on writing assumed that the client already knows the data type of the value it should write. This might not always be the case. If the client does not know the data type of a variable in advance, it can read it out of the meta data of the OPC UA server.

IList<ReadValueId> nodesToRead = new List<ReadValueId>
{
new ReadValueId()
{
NodeId = structureId,
AttributeId = Attributes.DataType
}
};
results = await Session.ReadAsync(nodesToRead, 0);

Afterwards the client can, in case of a structured data type, use a helper class of the SDK (DataTypeDefinitionManager) to generate a generic structure definition.

NodeId dataTypeId = (NodeId)results[0].Value;
GenericStructuredDataTypeDefinition definition = await DataTypeDefinitionManager.GetStructureDefinitionFromDataTypeIdAsync(dataTypeId);

The generic structure definition can be used to create a GenericEncodableStructure that can be filled with values and written to the server. Note that in the example the filling of the data in the code is only exemplified with some pre-knowledge of the data structure. Depending on the real client application you can for example generate a generic user interface to get the data from a user.

GenericEncodeableStructure DefaultStructure(GenericStructuredDataTypeDefinition definition)
{
var geo = new GenericEncodeableStructure(definition);
for (int i = 0; i < definition.Fields.Count; i++)
{
var field = definition.Fields[i];
if (field.ValueRank == ValueRanks.Scalar)
{
switch (field.BuiltInType)
{
case BuiltInType.ExtensionObject:
if (field.DataType is GenericStructuredDataTypeDefinition structure)
{
geo[i] = DefaultStructure(structure);
}
break;
case BuiltInType.Enumeration:
if (field.DataType is GenericEnumerationDataTypeDefinition enumeration)
{
geo[i] = new Variant(enumeration.Fields[0].Value);
}
break;
case BuiltInType.String:
geo[i] = new Variant("Generic Name");
break;
case BuiltInType.UInt16:
geo[i] = new Variant((UInt16)123);
break;
case BuiltInType.Float:
geo[i] = new Variant(13.37f);
break;
case BuiltInType.Guid:
geo[i] = new Variant(Uuid.NewUuid());
break;
case BuiltInType.DateTime:
geo[i] = new Variant(DateTime.UtcNow);
break;
case BuiltInType.LocalizedText:
geo[i] = new Variant(new LocalizedText("A description"));
break;
}
}
else if (field.ValueRank == ValueRanks.OneDimension)
{
switch (field.BuiltInType)
{
case BuiltInType.ExtensionObject:
if (field.DataType is GenericStructuredDataTypeDefinition structure)
{
ExtensionObjectCollection eos = new ExtensionObjectCollection()
{
new ExtensionObject(DefaultStructure(structure)),
new ExtensionObject(DefaultStructure(structure)),
new ExtensionObject(DefaultStructure(structure)),
new ExtensionObject(DefaultStructure(structure)),
};
geo[i] = new Variant(eos);
}
break;
}
}
}
return geo;
}
//Create encodeable object
var geo = DefaultStructure(definition);
IList<WriteValue> nodesToWrite = new List<WriteValue>
{
new WriteValue()
{
NodeId = structureId,
Value = new DataValue() { WrappedValue = geo },
AttributeId = Attributes.Value
}
};
List<StatusCode> res = await Session.WriteAsync(nodesToWrite);

Browse

The Browse Service is used to navigate through the address space. The Browse method in the SDK is a simplified toolkit method that uses only one node as starting point of the navigation. It returns a list of ReferenceDescription.

Next to the NodeId of the node to browse, you can pass a BrowseContext to the method. See the documentation of BrowseContext for detailed explanation of the class and its properties.

BrowseContext context = new BrowseContext()
{
BrowseDirection = BrowseDirection.Both,
ReferenceTypeId = ReferenceTypeIds.References,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.None,
MaxReferencesToReturn = 100
};

The Browse method provides an output argument called continuationPoint. With this argument a server can indicate that more references are available in the server for the node to browse. If the continuationPoint is null, the server returned all available ReferenceDescriptions. If it is not null, more ReferenceDescriptions are available. In this case the client can decide to either call BrowseNext to get the next ReferenceDescriptions or to call ReleaseContinuationPoint to indicate that the server can release the data that is required to return the next references.

results = m_session.Browse(
nodeToBrowse,
context,
null,
out m_continuationPoint);
results = Session.BrowseNext(ref m_continuationPoint);
Session.ReleaseBrowseContinuationPoint(m_continuationPoint);

TranslateBrowsePathsToNodeIds

In addition to the Browse Service, OPC UA supports the TranslateBrowsePathsToNodeIds Service. The main use case for this service is to program against types. Clients use the type definition (ObjectType) to program a user interface element, asset monitor, etc. and deploy this to the instances of the type. The Clients need to store the BrowsePath based on the type definition and use the TranslateBrowsePathsToNodeIds service to get the correct NodeIds of the instance. Unlike the Browse Service, this service follows several hops (levels in the hierarchy) at once, so that you only need to call this service once to dispatch all BrowsePaths of a type definition. In the example, the client loads the starting node and the BrowsePaths from the configuration.

NodeId startingNodeId = Settings.TranslateStartingNodeId;
List<BrowsePath> pathsToTranslate = new List<BrowsePath>();
BrowsePath browsePath = new BrowsePath
{
StartingNode = startingNodeId
};
browsePath.RelativePath.Elements.Add(new RelativePathElement()
{
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
IsInverse = false,
TargetName = Settings.TranslateElement
});
pathsToTranslate.Add(browsePath);

Afterwards, the service is called.

List<BrowsePathResult> results = Session.TranslateBrowsePath(pathsToTranslate);

Call Methods

When an OPC UA server supports methods, clients can call those methods with the OPC UA call service. The Client SDK provides a method on the session to call an OPC UA method. It takes the NodeId of the Object containing the method, the NodeId of the Method, and the input arguments of the method as input and the output arguments of the method as output. The example client calls a method as defined in its configuration.

Configuration.MethodDescription method = Settings.Method;
StatusCode result = Session.Call(method.ObjectId, method.MethodId, method.InputArguments, out inputArgumentErrors, out outputArguments);

RegisterNodes

The RegisterNodes service is used to provide potentially large NodeIds (e.g. containing a long string) to the server and get small NodeIds (integer-based) back from the server that can be used while the session is running. Clients can use this mechanism to optimize the size of a NodeId that they are using several times, for example by writing a Variable or calling a method periodically. Using this service only makes sense when a client wants to use the same NodeId several times, otherwise the large NodeId should be used directly. The Client SDK provides a method that calls the service which takes a list of NodeIds as inputs and returns a list of (small) NodeIds.

RegisteredNodes = Session.RegisterNodes(nodesToRegister, null);

When the client does not want to use the NodeIds anymore, it can release it with UnregisterNodes.

Access Historical Data

OPC UA allows to access the history of data, that is, changes of the value of a Variable over the time.

Read Raw Data

To read the history of a Variable, the Client needs to specify information which variable to access and which timespan to consider. In the example, the information of the Variable is first loaded from the configuration, and the timespan is defined.

HistoryReadValueIdCollection nodesToRead = new HistoryReadValueIdCollection();
foreach (NodeId node in Settings.HistoryVariableIds)
{
nodesToRead.Add(new HistoryReadValueId() { NodeId = node });
}
ReadRawModifiedDetails details = new ReadRawModifiedDetails()
{
StartTime = new DateTime(2017, 1, 1, 0, 0, 0, 0, DateTimeKind.Local),
EndTime = DateTime.UtcNow,
NumValuesPerNode = 10,
IsReadModified = false,
ReturnBounds = false,
};

Afterwards the service is called.

// fetch the history (with a 10s timeout).
List<HistoryDataReadResult> results = Session.HistoryReadRaw(
nodesToRead,
details,
new RequestSettings() { OperationTimeout = 10000 });

Like the Browse Service, HistoryRead might return a continuation point and might be called several times with the continuation point.

Update Historical Data

If the server supports this feature, clients can also change the historical data managed in the server. Therefore, they need to define the data to be updated. In the example, the information is loaded from the configuration.

List<UpdateDataDetails> nodesToUpdate = new List<UpdateDataDetails>();
IList<NodeId> historyIds = Settings.HistoryVariableIds;
foreach(NodeId nodeId in historyIds)
{
nodesToUpdate.Add(new UpdateDataDetails()
{
NodeId = nodeId,
PerformInsertReplace = PerformUpdateType.Update,
UpdateValues = new DataValueCollection()
{
new DataValue()
{
SourceTimestamp = DateTime.UtcNow,
ServerTimestamp = DateTime.UtcNow,
WrappedValue = new Variant(0.0)
}
}
});
}

And afterwards, the HistoryUpdate service is called.

var results = Session.HistoryUpdateData(nodesToUpdate);

Access Historical Events

Like the historical data of a Variable, OPC UA Clients can also access the history of events over the time.

Read Historical Events

To read the history of events, a client needs to specify the Object to receive the events, and in addition, an event filter and the timespan. In the example, this Object to read is loaded from the configuration.

IList<HistoryReadValueId> nodesToRead = HistoryReadValueIdsFromConfiguration();

The filter is also loaded from the configuration and the timespan is set.

var readEventDetails = new ReadEventDetails()
{
StartTime = new DateTime(2017, 1, 1, 0, 0, 0, 0, DateTimeKind.Local),
EndTime = DateTime.UtcNow,
NumValuesPerNode = 10,
Filter = new EventFilter()
{
SelectClauses = new SimpleAttributeOperandCollection()
}
};
foreach (QualifiedName name in EventFieldsForHistoryRead())
{
readEventDetails.Filter.SelectClauses.Add(
new SimpleAttributeOperand()
{
AttributeId = Attributes.Value,
BrowsePath = new QualifiedNameCollection()
{
name
}
});
}

Afterwards the HistoryRead service is called.

var results = Session.HistoryReadEvent(nodesToRead, readEventDetails);

Subscription services

Subscriptions monitor a set of MonitoredItems for Notifications. With this mechanism a server can send data changes or events to a client.

Create Subscription

A Subscription has several event handlers. In this example the DataChanged and the NewEvent event handler are implemented.

Calling Subscription.Create does the OPC UA service calls and creates the subscription at the server.

m_subscription = new Subscription(Session)
{
PublishingEnabled = true,
PublishingInterval = 100
};
if (Settings.ClientSettings.SubscriptionLifetime != 0)
{
m_subscription.Lifetime = Settings.ClientSettings.SubscriptionLifetime;
}
m_subscription.DataChanged += OnDataChanged;
m_subscription.NewEvents += OnEvent;
m_subscription.NotificationMessageReceived += OnNotificationMessageReceived;
m_subscription.StatusChanged += OnStatusChanged;
m_subscription.Recreated += OnSubscriptionRecreated;
m_subscription.Create();

Create DataMonitoredItems

If you want to create a MonitoredItem to get DataChanges, you need to create a DataMonitoredItem. You need to specify the NodeId and the AttributeId of the node to monitor. By default, the Value attribute is monitored.

In this example the DataMonitoredItems are created with the configured nodes.

IList<MonitoredItem> ItemsToMonitorFromConfiguration()
{
IList <MonitoredItem> result = new List<MonitoredItem>();
foreach (NodeId nodeId in Settings.ReadVariableIds)
{
DataMonitoredItem monitoredItem = new DataMonitoredItem(nodeId)
{
SamplingInterval = 0
};
result.Add(monitoredItem);
}
return result;
}

The resulting list is used to call CreateMonitoredItems. This call returns a list of StatusCodes indicating the success of the operation for each MonitoredItem.

IList<MonitoredItem> monitoredItems = ItemsToMonitorFromConfiguration();
try
{
List<StatusCode> results = m_subscription.CreateMonitoredItems(monitoredItems);

The DataChanges are reported with the DataChanged event handler to the application. The DataChangedEventArgs contains a list of DataChange structures. This structure contains the value that is sent by the server and the MonitoredItem that is related to the value. The client application does need to take care about the ClientHandle and the ServerHandle of the MonitoredItem. The application can set UserData at the MonitoredItem. This information can be used when a DataChange is reported.

void OnDataChanged(Subscription subscription, DataChangedEventArgs e)
{
//Process DataChanges in separate threads/tasks to avoid blocking the client application.
this.Application.ThreadPool.Queue(e, ProcessDataChange);
}

Create EventMonitoredItems

By creating an EventMonitoredItem the client application can subscribe to UA events on the server. The NodeId of the EventMonitoredItem represents the node in the address space that is the current root of the event hierarchy to report events to the client. The default configuration of the example uses the Server node. The Server object is a standardized node that is available in each server and provides all events of the OPC UA server.

EventMonitoredItem monitoredItem = new EventMonitoredItem(Settings.EventId)
{

You need to specify the event fields that the server sends with each event. This is done with the SelectClauses property of the ItemEventFilter.

Filter = new ItemEventFilter(m_subscription.Session.NamespaceUris)
{
SelectClauses =
{
BrowseNames.EventId,
BrowseNames.EventType,
BrowseNames.Message,
BrowseNames.Severity,
BrowseNames.SourceName,
BrowseNames.Time
}
}
};

Optionally you can set a WhereClause to restrict the sent events based regarding some conditions. The default configuration of the example filters for events of NodeId 1005 in the server namespace. If you want to try the example with the demo server you need to manually trigger events of that type by writing the Boolean Variable under Objects/Demo/004_Events/Trigger_SampleEvent. Each change of the variable triggers an event. You can use a second client (for example the UAExpert) to change the Variable on the Demo Server.

monitoredItem.Filter.WhereClauses.Add(FilterOperator.OfType, new LiteralOperand() { Value = new Variant(Settings.EventOfTypeFilter) });

The events are reported to the application by the NewEvents event handler.

void OnEvent(Subscription subscription, NewEventsEventArgs e)
{
//Process Events in separate threads/tasks to avoid blocking the client application.
this.Application.ThreadPool.Queue(e, ProcessEvent);
}
void ProcessEvent(object data, StatusCode error)
{
NewEventsEventArgs e = data as NewEventsEventArgs;
lock (ConsoleLock)
{
foreach (NewEvent anEvent in e.Events)
{
Console.WriteLine($"\nEvent occurred: {anEvent.Event.EventFields[2]}");
for (int ii = 0; ii < anEvent.Event.EventFields.Count; ii++)
{
Console.WriteLine($" {EventFieldName(anEvent.MonitoredItem.Filter.SelectClauses[ii].BrowsePath)}: {anEvent.Event.EventFields[ii]}");
}

Subscribe Alarm

Subscribing for alarms is essentially the same as subscribing for events. But there are two special features.

  • You can select an event field with an empty BrowseName. If this event field is selected, the ConditionId (NodeId) is returned in the event. You need the ConditionId to acknowledge alarms.
  • You can call the UA method ConditionRefresh to get events for each condition with the field Retain = true.

Since subscribing for alarms is technically the same as subscribing for events the same event handler is used to report the events.

/*00*/ monitoredItem.Filter.SelectClauses.AddConditionId();

To get all currently interesting alarms of a server, the ConditionRefresh method is called.

List<StatusCode> inputResults;
List<Variant> outArguments;
Session.Call(
ObjectTypeIds.ConditionType,
MethodIds.ConditionType_ConditionRefresh,
new List<Variant>() { m_subscription.SubscriptionId },
out inputResults,
out outArguments);
return ClientState.Connected;

Note that calling the refresh method triggers a ConditionRefreshStarted event, afterwards all interesting alarms are sent, and at the end, a ConfitionRefreshCompleted event is send.

Acknowledge Alarms

To acknowledge alarms, the Acknowledge Method needs to be called.

var result = Session.Call(
alarm.EventFields[0].ToNodeId(),
MethodIds.AcknowledgeableConditionType_Acknowledge,
new List<Variant>()
{
new Variant(alarm.EventFields[1].ToByteString()),
new Variant(new LocalizedText("Acknowledge from ConsoleClient"))
},
out inputArgumentResults,
out outputArguments);

You can acknowledge many (all) alarms with one service call.

foreach (var alarm in m_alarmList)
{
methods.Add(new CallMethodRequest()
{
ObjectId = alarm.EventFields[0].ToNodeId(),
MethodId = MethodIds.AcknowledgeableConditionType_Acknowledge,
InputArguments = new VariantCollection()
{
new Variant(alarm.EventFields[1].ToByteString()),
new Variant(new LocalizedText("Acknowledge all from ConsoleClient"))
}
});
}
var results = Session.CallList(methods, null);

Transfer Subscriptions

The lifetime of a session and a subscription are independent. Typically a subscription has a higher lifetime than a subscription. If a session gets lost (e.g. due to communication errors) the client can create a new session and still use the existing subscription of the previous session, without loosing data. Therefore, it needs to transfer the existing subscription from the previous session to the new session. In the example, the session is actively disconnected and afterwards a new session is created. With the TransferSubscription Service, the previously created subscription is transferred to the new session.

StatusCode ret = Session.TransferSingleSubscription(m_subscription);

Note that transferring a subscription only works when you use user credentials. This avoids that you can transfer a subscription from a different user / client. Therefore, this service call will fail when you have not set a user for the current session, and the same user for the previous session, where the subscription was created.