UA Bundle SDK .NET  2.4.1.366
 All Classes Namespaces Functions Variables Enumerations Enumerator Properties Events Modules Pages
.NET SDK Demo Server

The ANSI C SDK Demo Server is a collection of examples for developing different features of an OPC UA server and contains the Unified Automation Demo address space with nodes in the Demo folder.

The source code for the Unified Automation .NET based OPC UA demo server is included in the SDK. It contains an example (LargeArrayNodeManager.cs) for implementing information without the toolkit functionality.

The source code as a Visual Studio Solution can be found in Examples → UA Demo Server.

FileModel Example

The FileModel class implements support for the FileType object on the server side, which can be used to give an OPC UA client access to a file managed by the OPC UA server.

The .NET OPC UA Demo Server contains an example in the Address Space in the folder Objects → Demo → 014_Files. The example code can be found in the Visual Studio Solution for the Demo Server in the file Demo → DemoNodeManager.File.cs.

The following paragraphs describe the method SetupFileObject which creates a new file object. It takes the following arguments:

parentId
The NodeId of the parent node. The created File object will be referenced by an Organizes reference.
browseName
The browse name of the new File object. The browseName is also used to create the NodeId of the File object.
filePath
The file path on the disk.
description
Used for the description attribute of the new file object.

Create the OPC UA Nodes

NodeId objectId = new NodeId("Demo.Files." + browseName, DefaultNamespaceIndex);
CreateObjectSettings settings = new CreateObjectSettings()
{
ParentNodeId = parentId,
ReferenceTypeId = ReferenceTypeIds.Organizes,
RequestedNodeId = objectId,
BrowseName = new QualifiedName(browseName, DefaultNamespaceIndex),
TypeDefinitionId = ObjectTypeIds.FileType
};
if (description != null)
{
settings.Description = new LocalizedText(description);
}
CreateObject(Server.DefaultRequestContext, settings);

Create the Model Class

UnifiedAutomation.UaServer.FileModel

FileModel file = new FileModel();

Set the FileModel Properties

file.Writable = true;
file.UserWritable = true;
file.MaxFileSize = 10000;
file.FileOnDisk = new System.IO.FileInfo(filePath);

Create a Default File if None Exists

if (!file.FileOnDisk.Exists)
{
using (var ostrm = file.FileOnDisk.Open(System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None))
{
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(
"This file is used for demonstrating the OPC UA FileType.\n"
+ "You can call the Open method of the TextFile object with Mode argument 1 for ReadOnly.\n"
+ "You have to remember the returned FileHandle argument.\n"
+ "This FileHandle can be used for reading the file by calling the Read method and closing\n"
+ "the file by calling the Close method.");
ostrm.Write(bytes, 0, bytes.Length);
}
file.FileOnDisk.Refresh();
}

Set the File Size Depending on the File Content

file.Size = (ulong)file.FileOnDisk.Length;

LinkModelToNode

UnifiedAutomation.UaServer.BaseNodeManager.LinkModelToNode

LinkModelToNode(objectId, file, null, null, 500);

Write Implementation for DiscreteItemTypes

The DemoServer Address Space contains examples of the following Subtypes of the DiscreteItemType in Demo → 010_ComplianceTest → DA Profile → DataItemDiscreteType:

  1. TwoStateDiscreteType
  2. MultiStateDiscreteType
  3. MultiStateValueDiscreteType

TwoStateDiscreteType

The DataType of an instance of TwoStateDiscreteType is Boolean. The text representing the value is stored in the properties TrueState and FalseState. So it is not necessary to prevent clients from writing values that are not allowed.

MultiStateDiscreteType

Instances of a MultiStateDiscreteType have a property called EnumStrings. The value of this property is an array of LocalizedTexts. The corresponding text for a value can be found at the array index of the value of the property. E.g. the corresponding textual representation of the value 2 can be found at the array index 2 of the value of the property.

So the allowed values for an instance of MultiStateDiscreteType are:
0 ≤ allowed value < array length.

If a client tries to write a value that is not allowed, the server has to return BadOutOfRange (see example code below). The code snippet is taken from the class EnumStringDataSource which can be found in the file Demo → ValueDataStore.cs in the Visual Studio Solution for the Demo Server.

public override StatusCode Write(int componentIndex, Variant value, StatusCode status, DateTime timestamp)
{
if (timestamp == DateTime.MinValue)
{
timestamp = DateTime.UtcNow;
}
if (componentIndex == 0)
{
UnifiedAutomation.UaBase.TypeInfo typeInfo = TypeUtils.IsInstanceOfDataType(value, TypeInfo);
if (typeInfo == null)
{
return StatusCodes.BadTypeMismatch;
}
try
{
int newValue = value.ToInt32();
if (newValue >= 0 && newValue < EnumStrings.Length)
{
SelectedIndex = newValue;
Status = status;
Timestamp = timestamp;
return StatusCodes.Good;
}
}
catch (Exception)
{
// assume out of range error.
}
return StatusCodes.BadOutOfRange;
}

MultiStateValueDiscreteType

The behavior for an instance of MultiStateValueDiscretetType is quite similar. The instances have a property called EnumValues. The value of this property contains an array of EnumValueType. I.e. the value contains an array of value-text pairs. So the server has to check wheter the value of the property contains the value to write (see example below). The code snippet is taken from the class EnumValueDataSource which can be found in the file Demo → ValueDataStore.cs in the Visual Studio Solution for the Demo Server.

public override StatusCode Write(int componentIndex, Variant value, StatusCode status, DateTime timestamp)
{
if (timestamp == DateTime.MinValue)
{
timestamp = DateTime.UtcNow;
}
if (componentIndex == 0)
{
UnifiedAutomation.UaBase.TypeInfo typeInfo = TypeUtils.IsInstanceOfDataType(value, TypeInfo);
if (typeInfo == null)
{
return StatusCodes.BadTypeMismatch;
}
try
{
long newValue = value.ToInt64();
for (int ii = 0; ii < EnumValues.Length; ii++)
{
if (EnumValues[ii].Value == newValue)
{
SelectedIndex = ii;
Status = status;
Timestamp = timestamp;
return StatusCodes.Good;
}
}
}
catch (Exception)
{
// assume out of range error.
}
return StatusCodes.BadOutOfRange;
}

LocalizedData Example

The Demo Server contains an example for a variable containing localized data in the folder Objects → Demo → 011_UnicodeTest. The variable IDS_HELLO_WORLD has localizations for display name, description, and the value in English, German, and French.

The variable IDS_HELLO_WORLD is created in the method SetupLocalizedVariable which can be found in the file Demo → DemoNodeManager.cs in the Visual Studio Solution for the Demo Server.

The individual translations have to be added to the ResourceManager as shown in the following code snippet.

UnifiedAutomation.UaServer.ResourceManager

Server.ResourceManager.Add("IDS_HELLO_WORLD", "en-US", "Hello World");
Server.ResourceManager.Add("IDS_HELLO_WORLD", "de-DE", "Hallo Welt");
Server.ResourceManager.Add("IDS_HELLO_WORLD", "fr-CA", "Bonjour tout le monde");
Server.ResourceManager.Add("IDS_LIGHT_RED", "en-US", "Red");
Server.ResourceManager.Add("IDS_LIGHT_RED", "de-DE", "Rot");
Server.ResourceManager.Add("IDS_LIGHT_RED", "fr-CA", "Rouge");
Server.ResourceManager.Add("IDS_LIGHT_GREEN", "en-US", "Green");
Server.ResourceManager.Add("IDS_LIGHT_GREEN", "de-DE", "Grün");
Server.ResourceManager.Add("IDS_LIGHT_GREEN", "fr-CA", "Verte");

When in-memory nodes are read, the Translate method of the ResourceManager is called by the SDK automatically.

To test the translations, set the configuration parameter General.LocaleId of UaExpert to de-DE, connect to the .NET Demo Server and read the Variable. The german localizations of DisplayName, Description and Value will show up (see screenshot).

demoserver_localizeddata.png

ModelChangeEvent Example

ModelChange events can be fired by the server if nodes or references are added or deleted during runtime. A client that subscribes to ModelChanegEvents is able to detect these changes to the information model. E.g. UaExpert can refresh the information model tree view automatically so that the user doesn’t need to call rebrowse manually.

The folder 008_DynamicNodes in the address space contains a property called NodeVersion. The OPC UA Specification defines that such a property is required if ModelChangeEvents are being fired for a node. In the following example, a node will be added to the folder 008_DynamicNodes, resulting in a ModelChangeEvent being fired. Thus, it is necessary to add a NodeVersion property to the folder. In our example, the property is already part of the address space imported from the XML file.

The OPC UA specification defines the method CreateDynamicNode. The .NET implementation is the method DoCreateDynamicNode which can be found in the file DemoNodeManager.Methods.cs. The following code snippet shows how to add the node.

string name = "DynamicNode";
CreateVariableSettings settings = new CreateVariableSettings()
{
ParentNodeId = new NodeId("Demo.DynamicNodes", DefaultNamespaceIndex),
ReferenceTypeId = ReferenceTypeIds.HasComponent,
RequestedNodeId = new NodeId("Demo.DynamicNodes." + name, DefaultNamespaceIndex),
BrowseName = new QualifiedName(name, DefaultNamespaceIndex),
DisplayName = name,
AccessLevel = AccessLevels.CurrentReadOrWrite,
DataType = DataTypeIds.UInt32,
ValueRank = ValueRanks.Scalar,
Historizing = false,
TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
ValueType = NodeHandleType.ExternalPush,
ValueData = new DataSourceAddress(m_dynamicNode, 0)
};
VariableNode parentNode = CreateVariable(Server.DefaultRequestContext, settings);

In this example, the ModelChangeEvent is not fired directly from the method which creates the dynamic node. Instead the method AfterAddReference (which can be found in the file DemoNodeManager.cs) is used, which is called by the SDK after a reference has been added. The UpdateNodeVersion method tries to update the NodeVersion property and to fire the event.

First, the method checks the UserData wheter node versioning is supported.

NodeWithVersioning data = node.UserData as NodeWithVersioning;
if (data == null)
{
return false;
}

If so, the node version is updated.

lock (m_datasources)
{
DataVariableDataSource datasource = data.Source;
int version = datasource.Value.ToInt32();
version++;
datasource.Value = version.ToString();
ReportChange(context, datasource, 0);
}

Finally, the event is fired.

GenericEvent e = new GenericEvent(Server.FilterManager);
e.Initialize(
null,
ObjectTypeIds.GeneralModelChangeEventType,
ObjectIds.Server,
BrowseNames.Server,
"The address space has changed.");
ModelChangeStructureDataType[] changes = new ModelChangeStructureDataType[1];
changes[0] = new ModelChangeStructureDataType();
changes[0].Affected = node.NodeId;
changes[0].AffectedType = (instance != null) ? instance.TypeDefinitionId : null;
changes[0].Verb = (byte)verb;
e.Set(BrowseNames.Changes, new Variant(changes));
// report the event.
Server.ReportEvent(e);

User Authentication

The Demo Server contains an example that shows how to allow users to authenticate with different UserIdentityTokens. The following section gives an overview of the most important parts of the implementation. The complete source code can be found in the Visual Studio solution for the Demo Server (file TestServerManager.cs).

Adding an ImpersonateEventHandler to the SessionManager

This event handler is called in ActivateSession and passes the IdentityToken of the client.

this.SessionManager.ImpersonateUser += new ImpersonateEventHandler(SessionManager_ImpersonateUser);

and

private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArgs args)

Check for AnonymousIdentityToken

AnonymousIdentityToken anonynmousToken = args.NewIdentity as AnonymousIdentityToken;
if (anonynmousToken != null)
{
return;
}

Check for UserNameIdentityToken

The Demo Server accepts four hard-coded UserNameIdentityTokens as listed in the following table.

UserName Password
john master
joe god
sue curly
root secret
Note
For simplicity, the usernames and passwords in this example are specified in code. In real world applications, you should use some kind of database.

Users with Restricted Access

The users john, joe, and sue have been given restricted access. The code snippet shows the example code for the user john.

UserNameIdentityToken userNameToken = args.NewIdentity as UserNameIdentityToken;
if (userNameToken != null)
{
if (String.IsNullOrEmpty(userNameToken.UserName))
{
args.IdentityValidationError = StatusCodes.BadIdentityTokenInvalid;
return;
}
if (userNameToken.UserName == "john")
{
if (userNameToken.DecryptedPassword != "master")
{
args.IdentityValidationError = StatusCodes.BadUserAccessDenied;
}
return;
}

User root with Administrator Rights

To give the user root administrative rights, the EffectiveIdentity is set.

if (userNameToken.UserName == "root")
{
if (userNameToken.DecryptedPassword != "secret")
{
args.IdentityValidationError = StatusCodes.BadUserAccessDenied;
}
else
{
args.EffectiveIdentity = new UserIdentity(userNameToken, true);
}
return;
}

Logon with Username and Password of the Local Machine

The following code snippets show how to use username and password of the local windows installation for server logon.

LogonUser(userNameToken);
private static class Win32
{
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUser(
string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
ref IntPtr phToken);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public extern static bool CloseHandle(IntPtr handle);
}
private void LogonUser(UserNameIdentityToken identity)
{
IntPtr handle = IntPtr.Zero;
const int LOGON32_PROVIDER_DEFAULT = 0;
const int LOGON32_LOGON_NETWORK = 3;
bool result = Win32.LogonUser(
identity.UserName,
String.Empty,
identity.DecryptedPassword,
LOGON32_LOGON_NETWORK,
LOGON32_PROVIDER_DEFAULT,
ref handle);
if (!result)
{
throw new StatusException(StatusCodes.BadUserAccessDenied, "Login failed for user: {0}", identity.UserName);
}
}

Check for IssuedIdentityToken and KerberosIdentityToken

It is also possible to use an IssuedIdentityToken or an KerberosIdentityToken for user logon. The respective checks are not shown here, but can be found in sample code.

Assigning Access Rights to Nodes

The Demo Server contains example code for assigning access rights to nodes, which can be found in the file Demo → DemoNodeManager.AccessControl.cs and AccessControlManager.cs in the Visual Studio Solution for the Demo Server. It shows how to control the read and write access for Variable values and the browse access for specific nodes.

There are several virtual HasAccess methods defined at the BaseNodeManager. These methods control whether some specific content is send to a client. The default implementation always returns true.

In the example, all knowledge needed for access control is hard-coded. In real world applications, the information should be received by some kind of database.

The folder containing nodes with access rights that are different for specific users can be found in the folder Objects → Demo → 005_AccessRights in the Demo Server address space. There are several subfolders:

Access_All
contains Variables having value attributes that can be read and written by everybody.
Access_John
contains Variables having value attributes that can be read and written by john only (John_RO, John_RW, John_WO) and Variables where john has full access and other users have restricted access
Access_Operators
contains Variables having value attributes that can be read and written by users of the operators group (Operators_RO, Operators_RW, Operators_WO) and Variables where john has full access and other users have restricted access.
Browse_All
can be browsed by all users.
Browse_John
can only be browsed by the user john.
Browse_Operators
can be browsed by users of the operators group.

A list of users and their passwords can be found in the description of the User Authentication example.

In this example, the class AccessControlManager

  • manages the users (AccessControlManager.AddUser),
  • has hard coded user groups (enum AccessControlGroup),
  • and checks whether a node’s content can be accessed.

To use this class, we add the AccessControlSettings to the UserData of a node (see DemoNodeManager.SetupAccessControl()). When the SDK calls the methods

  • HasAccess(RequestContext context, BrowseHandle browseHandle)
  • or HasAccess(RequestContext context, NodeAttributeHandle nodeHandle, UserAccessMask accessMasks),

the UserData is checked. If the UserData has the correct type, the HasAccess method of the AccessControlManager is called.

Exposing Structured Data Type Members in the Address Space

Overview

There are use cases where one might want to create VariableTypes that expose fields of a structure to the children of the VariableType:

  • Clients expected to connect to the server cannot deal with structured DataTypes.
  • Clients are only interested in accessing specific fields of the structure.

There are several VariableTypes in namespace 0 having children which expose fields of the structure. An example is ServerStatusType. The DataType of its Value attribute is a structured DataType, the ServerStatusDataType. This structure has the fields StartTime, CurrentTime, State, BuildInfo SecondsTillShutdown, and ShutdownReason. For each of these fields, the ServerStatusType has a corresponding child. The instance of this type in the Demo Server address space is the ServerStatus variable of the Server object.

This example describes a simplified case which is also shown in the Demo Server: The structured DataType WorkOrderType and the corresponding WorkOrderVariableType. The instances of the latter are WorkOrderVariable and WorkOrderVariable2 in the folder Objects → Demo → 015_WorkOrder in the Demo Server Address Space.

When using such a scenario, the developer has to ensure that when the value of the instance is changed, the value of the child is changed as well (and vice versa).

Implementation

The implementation can be found in the files Demo → DemoNodeManager.cs and Controllers → WorkOrderVariableModelController.cs

The Type nodes and the instance nodes are created when importing the Demo Server address space from the file demoserver.xml, which is not explained in this section.

We use the method LinkModelToNode to keep the values of the instances of WorkOrderVariableType and its children consistent (see DemoNodeManager.cs).

private void SetupWorkOrderModel()
{
NodeId WorkOrderId = new NodeId(Model.Variables.Demo_WorkOrder_WorkOrderVariable, DefaultNamespaceIndex);
Model.WorkOrderType value = new Model.WorkOrderType();
SetVariableDefaultValue(WorkOrderId, new Variant(value));
Model.WorkOrderVariableModel model = new Model.WorkOrderVariableModel()
{
Value = value
};
LinkModelToNode(WorkOrderId, model, null, null, 0);
WorkOrderId = new NodeId(Model.Variables.Demo_WorkOrder_WorkOrderVariable2, DefaultNamespaceIndex);
value = new Model.WorkOrderType();
SetVariableDefaultValue(WorkOrderId, new Variant(value));
model = new Model.WorkOrderVariableModel()
{
Value = value
};
LinkModelToNode(WorkOrderId, model, null, null, 0);
}

The remaining parts of the implementation can be found in the file WorkOrderVariableModelController.cs.

We have to extend the auto-generated class WorkOrderVariableModel by overriding GetModelHandle to assign custom getters and setters.

Check arguments:

private WorkOrderVariableModel m_model;
m_model = instance as WorkOrderVariableModel;
if (m_model == null)
{
return null;
}

Get a default model handle

ModelHandle handle = ModelMapper.GetModelHandle(namespaceUris, m_model);

Assign custom handlers to each field

ushort nsIdx = (ushort)namespaceUris.IndexOf(Model.Namespaces.Model);
foreach (ModelMapping mapping in handle.Mappings)
{
if (mapping.BrowsePath.Length == 1)
{
if (mapping.BrowsePath[0] == new QualifiedName(BrowseNames.AssetID, nsIdx))
{
mapping.MappingData = new Delegate[] {
// the ModelMappingGetValueHandler shall be the first handler in the array
new ModelMappingGetValueHandler(GetAssetID),
// the ModelMappingSetValueHandler shall be the second handler in the array
};
}
private DataValue GetAssetID(IMapperContext context)
{
return new DataValue()
{
WrappedValue = new Variant(m_model.Value.AssetID),
SourceTimestamp = DateTime.UtcNow,
ServerTimestamp = DateTime.UtcNow
};
}
private void SetAssetID(IMapperContext context, DataValue AssetId)
{
m_model.Value.AssetID = AssetId.WrappedValue.ToString();
}