.NET Based OPC UA Client/Server SDK  3.1.0.500
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 .NET Frameworks 3.5, 4.0, 4.5 and 4.8 and for .NET Core. 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 is triggered.

Configuration

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 IdentityToken is null. In this case an anonymous user will connect. When selecting "set username and password" the username and password set in the configuration are 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. With the securitySelection you can specify if you want to connect to the endpoint with SecurityPolicy http://opcfoundation.org/UA/SecurityPolicy#None or to the endpoint with the best SecurityPolicy. The Client SDK makes sure that only endpoints with supported security policies get chosen. If you want to connect without security and no endpoint with policy None exists or if you want to connect with security and no secure endpoint exists, 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 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 needed, when 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 appliction binds the port {0}", 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 use security a SecurityProvider must be set to the ApplicationInstanceBase. The SDK includes two implementations of ISecurityProvider. 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 .NET Framework applications. 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 NETCOREAPP
var securityProvider = new BouncyCastleSecurityProvider();
#else
var securityProvider = new WindowsSecurityProvider();
#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 EventHandler ApplicationInstanceBase.UntrustedCertificate get called if a certificate is sent to the application that has not been trusted yet.

If the property UntrustedCertificateEventArgs.Accept is set to 'true' within the EventHandler, the certificate is accepted. If the property UntrustedCertificateEventArgs.Persist is set to 'true' within the EventHandler, the certificate is 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 EventHandler 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: {0}", e.Certificate);
Console.WriteLine("-------------------------------------------------------");
Console.WriteLine(" - Press 0 to accept the certificate -");
}
else
{
Console.WriteLine(" - Validation error in certificate: {0}, {1}", 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: {0}", e.Certificate);
Console.WriteLine("-------------------------------------------------------");
}
else
{
Console.WriteLine(" - Validation error in certificate: {0}, {1}", e.ValidationError, e.Certificate);
Console.WriteLine("-------------------------------------------------------");
}
Console.WriteLine(" - Further validation errors:");
for (int ii = 1; ii < validationErrors.Count; ii++)
{
Console.WriteLine(" - {0}", 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;
}
}
}

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 OPC UA service calls.

Session.ChangeUser();

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 Connect 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 call 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 ReadValueId 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 has four arguments. The first argument is the list of nodes to read. The second argument indicates the max age of the variables. It indicates if a 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 this. 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 WriteValueId 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 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>();
nodesToRead.Add(new ReadValueId()
{
NodeId = structureId,
AttributeId = Attributes.DataType
});
results = Session.Read(nodesToRead);

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

NodeId datatypeId = (NodeId)results[0].Value;
ExpandedNodeId eNodeId = new ExpandedNodeId(datatypeId.IdType, datatypeId.Identifier, Session.NamespaceUris[datatypeId.NamespaceIndex], datatypeId.NamespaceIndex);
GenericStructureDataType newType = DataTypeManager.NewTypeFromDataType(eNodeId, BrowseNames.DefaultBinary, true);

The generic structure can be used to create a GenericEncodableObject 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.

//Create encodeable object
var geo = new GenericEncodeableObject(newType);
//set fields of encodeable object
for( int i = 0; i < newType.Fields.Count; ++i)
{
if (newType[i].TypeDescription.TypeClass == GenericDataTypeClass.Structured)
{
var subGeo = new GenericEncodeableObject(newType[i].TypeDescription);
}
if (geo[i].TypeInfo == TypeInfo.Scalars.String)
{
geo[i] = new Variant("Generic Person name");
}
else if (geo[i].TypeInfo == TypeInfo.Scalars.UInt16)
{
geo[i] = new Variant((UInt16)123);
}
else if (geo[i].TypeInfo == TypeInfo.Scalars.Float)
{
geo[i] = new Variant(13.37f);
}
}
IList<WriteValue> nodesToWrite = new List<WriteValue>();
nodesToWrite.Add(new WriteValue()
{
NodeId = structureId,
Value = new DataValue() { WrappedValue = geo },
AttributeId = Attributes.Value
});
List<StatusCode> res = Session.Write(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 call BrowseNext to get the next ReferenceDescriptions or to call ReleaseContinuationPoint to indicate that the server can release 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();
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, first the information of the Variable is 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 EventHandlers. In this example the DataChanged and the NewEvent EventHandler are implemented.

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

m_subscription = new Subscription(Session);
m_subscription.PublishingEnabled = true;
m_subscription.PublishingInterval = 100;
m_subscription.Lifetime = Settings.ClientSettings.SubscriptionLifetime;
m_subscription.DataChanged += new DataChangedEventHandler(OnDataChanged);
m_subscription.NewEvents += OnEvent;
m_subscription.NotificationMessageReceived += OnNotificationMessageReceived;
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.

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

private void OnDataChanged(Subscription subscription, DataChangedEventArgs e)
{
lock (ConsoleLock)
{
for (int i = 0; i < e.DataChanges.Count; i++)
{
// Print result for variable - check first the result code
if (StatusCode.IsGood(e.DataChanges[i].Value.StatusCode))
{
// The node succeeded - print the value as string
if (e.DataChanges[i].MonitoredItem.UserData != null)
{
Console.Write(e.DataChanges[i].MonitoredItem.UserData);
}
else
{
Console.Write(e.DataChanges[i].MonitoredItem.NodeId);
}
Console.WriteLine(": " + e.DataChanges[i].Value.WrappedValue.ToString());
}
}
}
}

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 (UnifiedAutomation.UaBase.ObjectIds.Server). 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.

monitoredItem.Filter = new UnifiedAutomation.UaClient.ItemEventFilter(m_subscription.Session.NamespaceUris);
// build the filter.
monitoredItem.Filter.SelectClauses.Add(BrowseNames.EventId);
monitoredItem.Filter.SelectClauses.Add(BrowseNames.EventType);
monitoredItem.Filter.SelectClauses.Add(BrowseNames.Message);
monitoredItem.Filter.SelectClauses.Add(BrowseNames.Severity);
monitoredItem.Filter.SelectClauses.Add(BrowseNames.SourceName);
monitoredItem.Filter.SelectClauses.Add(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 EventHandler.

private void OnEvent(Subscription subscription, NewEventsEventArgs e)
{
lock (ConsoleLock)
{
foreach (NewEvent anEvent in e.Events)
{
Console.WriteLine("\nEvent occured: " + anEvent.Event.EventFields[2].ToString());
for (int ii = 0; ii < anEvent.Event.EventFields.Count; ii++)
{
Console.WriteLine(" {0}: {1}",
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 EventHandler 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.