How-to guide
NodeSets
Import information models as NodeSets into the server and implement their nodes.
How-to guide
NodeSets
NodeSets describe the content of the address space of a server in XML (eXtensible Markup Language). The description defines data types with their transported logical structure (data-dependent information) and their physical structure (in memory). A NodeSet also contains the definitions of node types and of concrete node instances. The root element UANodeSet also describes the references between the nodes. This applies not only to the nodes from this NodeSet but also to nodes from other NodeSets and from the specification.
Companion specifications not only extend the general definition of the address space but also bring their own data types and node types, among others. Each companion specification therefore comes with one or more NodeSets. For a server to comply with a companion specification, it must implement the types and behavior defined there. The Companion specifications section shows what a Companion SDK takes care of.
NodeSets also describe the address space in PLC engineering. You can export the entire configuration of a PLC from engineering software such as TIA Portal. With this export, you can initialize the OPC UA server of the PLC.
Whatever the source, if a server should use a NodeSet, it must provide the logic to import and implement the address space described in it. The following sections show how this works.
Import NodeSets
Types usedOpcNodeSetOpcNodeSetManagerOpcNodeManagerOpcNodeSetImportOptions
With the OpcNodeSetManager, you import the nodes that a NodeSet describes 1:1:
var umatiManager = OpcNodeSetManager.Create(
OpcNodeSet.Load(@".\umati.xml"),
OpcNodeSet.Load(@".\umati-instances.xml"));
using (var server = new OpcServer("opc.tcp://localhost:4840/", umatiManager)) {
server.Start();
...
}Dim umatiManager = OpcNodeSetManager.Create( _
OpcNodeSet.Load(".\umati.xml"), _
OpcNodeSet.Load(".\umati-instances.xml"))
Using server As New OpcServer("opc.tcp://localhost:4840/", umatiManager)
server.Start()
' ...
End UsingWhen you call OpcNodeSetManager.Create, you can specify one or more NodeSets. The resulting OpcNodeManager imports the NodeSets. When the server starts, it creates the nodes defined in them in the address space of the server. Besides a file path, the OpcNodeSet.Load method also accepts a Stream, for example for a NodeSet embedded in the application as a resource. With the GetNodes and GetNode methods, you can also evaluate the nodes described in the NodeSet:
using (var stream = File.OpenRead(@".\umati.xml")) {
var nodeSet = OpcNodeSet.Load(stream);
foreach (var node in nodeSet.GetNodes())
Console.WriteLine("{0}: {1}", node.Id, node.DisplayName);
}Using stream = File.OpenRead(".\umati.xml")
Dim nodeSet = OpcNodeSet.Load(stream)
For Each node In nodeSet.GetNodes()
Console.WriteLine("{0}: {1}", node.Id, node.DisplayName)
Next
End UsingThe ImportOptions property of the OpcNodeSetManager controls how the node manager creates the imported nodes (default OpcNodeSetImportOptions.None):
CompleteDataTypeDefinitionFields: adds the fields of its base types to the DataTypeDefinition of a structure type.CompleteDataTypeDefinitionDefaultEncoding: adds a missingDefaultEncodingIdto the DataTypeDefinition.CreateNonGenericNodeInstances: creates the typed classes of the OPC UA .NET SDK for the imported nodes instead of generic instances.
The first two options apply to all NodeSets of the server:
umatiManager.ImportOptions = OpcNodeSetImportOptions.CompleteDataTypeDefinitionFields
| OpcNodeSetImportOptions.CreateNonGenericNodeInstances;umatiManager.ImportOptions = OpcNodeSetImportOptions.CompleteDataTypeDefinitionFields _
Or OpcNodeSetImportOptions.CreateNonGenericNodeInstancesIf a custom node manager should import a NodeSet instead, override the ImportNodes method of the OpcNodeManager class:
protected override IEnumerable<OpcNodeSet> ImportNodes()
{
yield return OpcNodeSet.Load(@".\umati.xml");
yield return OpcNodeSet.Load(@".\umati-instances.xml");
}Protected Overrides Iterator Function ImportNodes() As IEnumerable(Of OpcNodeSet)
Yield OpcNodeSet.Load(".\umati.xml")
Yield OpcNodeSet.Load(".\umati-instances.xml")
End FunctionConnect imported NodeSets to logic
Types usedOpcNodeManagerIOpcNode
Importing a NodeSet alone is often not enough, because the logic that connects the underlying system is still missing. This logic maps, for example, reading a node to reading a word in a data block. The same applies to writing a node.
If you create the node manager through OpcNodeSetManager.Create, you set the logic through the ImplementNodeCallback property of the OpcNodeSetManager. The SDK calls the callback for each imported node. For a UMATI NodeSet, the logic that simulates the status of a lamp could look like this:
var lampTypeId = OpcNodeId.Parse("ns=2;i=1041");
var random = new Random();
var umatiManager = OpcNodeSetManager.Create(
OpcNodeSet.Load(@".\umati.xml"),
OpcNodeSet.Load(@".\umati-instances.xml"));
umatiManager.ImplementNodeCallback = (context, node) => {
var variableNode = node as OpcVariableNode;
if (variableNode == null || variableNode.Name != "2:Status")
return;
var objectNode = variableNode.Parent as OpcObjectNode;
if (objectNode != null && objectNode.TypeDefinitionId == lampTypeId) {
variableNode.ReadVariableValueCallback = (readContext, value)
=> new OpcVariableValue<object>(random.Next(minValue: 0, maxValue: 2));
}
};Dim lampTypeId = OpcNodeId.Parse("ns=2;i=1041")
Dim random = New Random()
Dim umatiManager = OpcNodeSetManager.Create( _
OpcNodeSet.Load(".\umati.xml"), _
OpcNodeSet.Load(".\umati-instances.xml"))
umatiManager.ImplementNodeCallback = Sub(context, node)
Dim variableNode = TryCast(node, OpcVariableNode)
If variableNode Is Nothing OrElse variableNode.Name <> "2:Status" Then
Return
End If
Dim objectNode = TryCast(variableNode.Parent, OpcObjectNode)
If objectNode IsNot Nothing AndAlso objectNode.TypeDefinitionId = lampTypeId Then
variableNode.ReadVariableValueCallback = Function(readContext, value) _
New OpcVariableValue(Of Object)(random.Next(minValue:=0, maxValue:=2))
End If
End SubIn a custom node manager, you override the OpcNodeManager.ImplementNode method for this logic instead. The SDK calls the method for each node that is imported or created through CreateNodes. After the server has started, it also calls the method for each node that a client adds to the address space of the node manager. Call the base implementation in the method. Among other things, it adds the references to the interfaces (HasInterface) that a node implements:
protected override void ImplementNode(IOpcNode node)
{
// Keeps the default implementation, e.g. the references to the interfaces of the node.
base.ImplementNode(node);
// Implement your Node(s) here.
}Protected Overrides Sub ImplementNode(ByVal node As IOpcNode)
'Keeps the default implementation, e.g. the references to the interfaces of the node.
MyBase.ImplementNode(node)
'Implement your Node(s) here.
End SubFor the UMATI NodeSet shown above, the implementation in a custom node manager looks like this:
private static readonly OpcNodeId LampTypeId = "ns=2;i=1041";
private readonly Random random = new Random();
protected override void ImplementNode(IOpcNode node)
{
if (node is OpcVariableNode variableNode && variableNode.Name == "2:Status") {
if (variableNode?.Parent is OpcObjectNode objectNode && objectNode.TypeDefinitionId == LampTypeId) {
variableNode.ReadVariableValueCallback = (context, value) => new OpcVariableValue<object>(this.random.Next(0, 2));
}
}
}Private Shared ReadOnly LampTypeId As OpcNodeId = "ns=2;i=1041"
Private random As New Random()
Protected Overrides Sub ImplementNode(node As IOpcNode)
Dim variableNode = TryCast(node, OpcVariableNode)
If variableNode IsNot Nothing AndAlso variableNode.Name = "2:Status" Then
Dim objectNode = TryCast(variableNode.Parent, OpcObjectNode)
If objectNode IsNot Nothing AndAlso objectNode.TypeDefinitionId = LampTypeId Then
variableNode.ReadVariableValueCallback = Function(context, value) New OpcVariableValue(Of Object)(Me.random.Next(0, 2))
End If
End If
End SubCompanion specifications
Types usedOpcServerOpcNodeManagerOpcNodeSetManagerOpcNamespaceOpcObjectNodeOpcNodeModel
You can import and implement a companion specification like any other NodeSet. For many companion specifications, there is also a separate package of the SDK (Companion SDK), for example Opc.UaFx.Advanced.Machinery for OPC UA for Machinery (OPC 40001-1) or Opc.UaFx.Advanced.ISA95.JobControl for ISA-95 Job Control (OPC 10031-4). The OPC UA Companion SDKs product page lists the available Companion SDKs. Using OPC UA for Machinery as an example, the following sections show what such a package includes and which steps you have to do by hand without one.
What a Companion SDK includes
| Component | Content, using Opc. as an example |
|---|---|
| Embedded NodeSet | The NodeSet of the specification is embedded in the assembly; you neither have to obtain it nor ship it. |
| Dependencies | The server class Opc includes the node managers of all required specifications (DI, IA, and Machinery). |
| Constants | Opc contains the namespace URI, the namespace (Namespace, GetName), and the OpcNodeId of all types and instances of the specification, for example MachineIdentificationType or MachinesNode. |
| Typed node classes | A class for each object type, for example Opc, with a property for each child node (Manufacturer, SerialNumber, YearOfConstruction, and so on). When you create it, the class creates the child nodes with the name, namespace, and data type from the specification. |
| Interfaces | The interfaces of the specification are implemented as .NET interfaces. The SDK creates the corresponding HasInterface references automatically when it implements the nodes. |
| Typed methods | Methods of the specification are implemented as overridable methods with typed parameters. Without an implementation, they throw a Not. |
| Node manager | Opc imports the embedded NodeSet, resolves the namespace, and provides the Machines folder. |
| Client class | Opc registers the data types of the specification and its dependencies and resolves the namespaces after connecting, so that the constants from Opc also apply in the client. |
Machine with a Companion SDK
The following example provides a machine with its identification according to OPC UA for Machinery. The OpcMachineIdentificationNode class creates the child nodes of the identification itself. You only set the values:
using (var server = new OpcMachineryServer("opc.tcp://localhost:4840/")) {
server.Start();
OpcMachineryNodeManager machinery = server.NodeManagers
.OfType<OpcMachineryNodeManager>()
.First();
var machine = new OpcObjectNode(machinery.Machines, "CNC-042");
var identification = new OpcMachineIdentificationNode(machine, "Identification");
identification.Manufacturer.Value = "Example Machines Ltd.";
identification.SerialNumber.Value = "SN-0042";
identification.ProductInstanceUri.Value = "urn:example:cnc-0042";
machinery.AddNode(server.SystemContext, machine);
}Using server = New OpcMachineryServer("opc.tcp://localhost:4840/")
server.Start()
Dim machinery As OpcMachineryNodeManager = server.NodeManagers _
.OfType(Of OpcMachineryNodeManager)() _
.First()
Dim machine = New OpcObjectNode(machinery.Machines, "CNC-042")
Dim identification = New OpcMachineIdentificationNode(machine, "Identification")
identification.Manufacturer.Value = "Example Machines Ltd."
identification.SerialNumber.Value = "SN-0042"
identification.ProductInstanceUri.Value = "urn:example:cnc-0042"
machinery.AddNode(server.SystemContext, machine)
End UsingUnless you specify otherwise, a node class of a Companion SDK creates all child nodes of its type, including the optional ones. An OpcNodeModel controls which optional child nodes it creates. The Node models page describes this.
Machine without a Companion SDK
Without a Companion SDK, the same server requires these steps:
- Obtain the NodeSet files of the specification and all its dependencies, ship them with the application, and load them in the correct order (DI, IA, Machinery).
- Resolve the namespaces of the specifications by their URI to build the
OpcNodeIdandOpcNameof the types and instances. - Create the instance and set its type definition (
TypeDefinitionId). - Create each child node yourself with the name, namespace, and data type from the specification.
- Add the references to the interfaces of the type (HasInterface) by hand.
public class HandmadeMachineManager : OpcNodeManager
{
private static readonly OpcNodeId HasInterface = OpcNodeId.Parse("i=17603");
public HandmadeMachineManager()
: base("http://example.com/machines/")
{
}
public static OpcServer CreateServer()
{
var nodeSets = OpcNodeSetManager.Create(
OpcNodeSet.Load(@"NodeSets\Opc.Ua.Di.NodeSet2.xml"),
OpcNodeSet.Load(@"NodeSets\Opc.Ua.IA.NodeSet2.xml"),
OpcNodeSet.Load(@"NodeSets\Opc.Ua.Machinery.NodeSet2.xml"));
return new OpcServer("opc.tcp://localhost:4840/", nodeSets, new HandmadeMachineManager());
}
protected override IEnumerable<IOpcNode> CreateNodes(OpcNodeReferenceCollection references)
{
var di = OpcNamespace.Create("http://opcfoundation.org/UA/DI/");
var machinery = OpcNamespace.Create("http://opcfoundation.org/UA/Machinery/");
di.Resolve(this);
machinery.Resolve(this);
// Machinery: Machines folder (i=1001).
var machine = new OpcObjectNode("CNC-042");
references.Add(
machine,
machinery.GetId(1001),
direction: OpcReferenceDirection.ChildToParent,
referenceType: OpcReferenceType.Organizes);
// Machinery: MachineIdentificationType (i=1012).
var identification = new OpcObjectNode(machine, machinery.GetName("Identification"));
identification.TypeDefinitionId = machinery.GetId(1012);
// Mandatory properties: names, namespaces and data types from the specification.
var manufacturer = new OpcTextPropertyNode(
identification,
di.GetName("Manufacturer"),
value: new OpcText("Example Machines Ltd."));
var serialNumber = new OpcPropertyNode<string>(
identification,
di.GetName("SerialNumber"),
id: OpcNodeId.Null,
value: "SN-0042");
var instanceUri = new OpcPropertyNode<string>(
identification,
di.GetName("ProductInstanceUri"),
id: OpcNodeId.Null,
value: "urn:example:cnc-0042");
// Interfaces of the type: IMachineVendorNameplateType (i=1010),
// IMachineTagNameplateType (i=1011).
references.Add(
identification,
machinery.GetId(1010),
direction: OpcReferenceDirection.ParentToChild,
referenceTypeId: HasInterface);
references.Add(
identification,
machinery.GetId(1011),
direction: OpcReferenceDirection.ParentToChild,
referenceTypeId: HasInterface);
yield return machine;
}
}Public Class HandmadeMachineManager
Inherits OpcNodeManager
Private Shared ReadOnly HasInterface As OpcNodeId = OpcNodeId.Parse("i=17603")
Public Sub New()
MyBase.New("http://example.com/machines/")
End Sub
Public Shared Function CreateServer() As OpcServer
Dim nodeSets = OpcNodeSetManager.Create( _
OpcNodeSet.Load("NodeSets\Opc.Ua.Di.NodeSet2.xml"), _
OpcNodeSet.Load("NodeSets\Opc.Ua.IA.NodeSet2.xml"), _
OpcNodeSet.Load("NodeSets\Opc.Ua.Machinery.NodeSet2.xml"))
Return New OpcServer("opc.tcp://localhost:4840/", nodeSets, New HandmadeMachineManager())
End Function
Protected Overrides Iterator Function CreateNodes( _
ByVal references As OpcNodeReferenceCollection) As IEnumerable(Of IOpcNode)
Dim di = OpcNamespace.Create("http://opcfoundation.org/UA/DI/")
Dim machinery = OpcNamespace.Create("http://opcfoundation.org/UA/Machinery/")
di.Resolve(Me)
machinery.Resolve(Me)
'Machinery: Machines folder (i=1001).
Dim machine = New OpcObjectNode("CNC-042")
references.Add( _
machine, _
machinery.GetId(1001), _
direction:=OpcReferenceDirection.ChildToParent, _
referenceType:=OpcReferenceType.Organizes)
'Machinery: MachineIdentificationType (i=1012).
Dim identification = New OpcObjectNode(machine, machinery.GetName("Identification"))
identification.TypeDefinitionId = machinery.GetId(1012)
'Mandatory properties: names, namespaces and data types from the specification.
Dim manufacturer = New OpcTextPropertyNode( _
identification, _
di.GetName("Manufacturer"), _
value:=New OpcText("Example Machines Ltd."))
Dim serialNumber = New OpcPropertyNode(Of String)( _
identification, _
di.GetName("SerialNumber"), _
id:=OpcNodeId.Null, _
value:="SN-0042")
Dim instanceUri = New OpcPropertyNode(Of String)( _
identification, _
di.GetName("ProductInstanceUri"), _
id:=OpcNodeId.Null, _
value:="urn:example:cnc-0042")
'Interfaces of the type: IMachineVendorNameplateType (i=1010),
'IMachineTagNameplateType (i=1011).
references.Add( _
identification, _
machinery.GetId(1010), _
direction:=OpcReferenceDirection.ParentToChild, _
referenceTypeId:=HasInterface)
references.Add( _
identification, _
machinery.GetId(1011), _
direction:=OpcReferenceDirection.ParentToChild, _
referenceTypeId:=HasInterface)
Yield machine
End Function
End ClassThe identifiers of the nodes (for example, i=1001 for the Machines folder or i=1012 for MachineIdentificationType) come from the NodeSet of the specification. The SDK offers no constant for the HasInterface reference type. The example therefore uses its OpcNodeId (i=17603).
Implement methods
The effort also differs for methods of the specification. Without a Companion SDK, you look up the imported method node by its name and implement it through a command (IOpcMethodCommand). You take the order and types of the arguments from the specification. The arguments arrive as untyped lists:
nodeSets.ImplementNodeCallback = (context, node) => {
var method = node as OpcMethodNode;
if (method != null && method.Name.Value == "TransferToDevice")
method.Command = new TransferToDeviceCommand();
};
...
public sealed class TransferToDeviceCommand : IOpcMethodCommand
{
public bool CanExecute(OpcContext context)
{
return true;
}
public bool CanUserExecute(OpcContext context)
{
return true;
}
public void Execute(
OpcMethodContext context,
IList<object> inputArguments,
IList<object> outputArguments)
{
// Order and types of the arguments as defined by the specification.
outputArguments[0] = 1; // TransferID
outputArguments[1] = 0; // InitTransferStatus
}
}nodeSets.ImplementNodeCallback = Sub(context, node)
Dim method = TryCast(node, OpcMethodNode)
If method IsNot Nothing AndAlso method.Name.Value = "TransferToDevice" Then
method.Command = New TransferToDeviceCommand()
End If
End Sub
...
Public NotInheritable Class TransferToDeviceCommand
Implements IOpcMethodCommand
Public Function CanExecute(ByVal context As OpcContext) As Boolean _
Implements IOpcMethodCommand.CanExecute
Return True
End Function
Public Function CanUserExecute(ByVal context As OpcContext) As Boolean _
Implements IOpcMethodCommand.CanUserExecute
Return True
End Function
Public Sub Execute( _
ByVal context As OpcMethodContext, _
ByVal inputArguments As IList(Of Object), _
ByVal outputArguments As IList(Of Object)) _
Implements IOpcMethodCommand.Execute
'Order and types of the arguments as defined by the specification.
outputArguments(0) = 1 'TransferID
outputArguments(1) = 0 'InitTransferStatus
End Sub
End ClassWith a Companion SDK, you only override the typed method of the node class, here using the TransferToDevice method from OPC UA for Devices (DI) as an example:
public class DeviceTransferServices : OpcTransferServicesNode
{
public DeviceTransferServices(IOpcNode parent)
: base(parent, "TransferServices")
{
}
public override void TransferToDevice(
OpcMethodContext methodContext,
out int transferID,
out int initTransferStatus)
{
transferID = 1;
initTransferStatus = 0;
}
}Public Class DeviceTransferServices
Inherits OpcTransferServicesNode
Public Sub New(ByVal parent As IOpcNode)
MyBase.New(parent, "TransferServices")
End Sub
Public Overrides Sub TransferToDevice( _
ByVal methodContext As OpcMethodContext, _
ByRef transferID As Integer, _
ByRef initTransferStatus As Integer)
transferID = 1
initTransferStatus = 0
End Sub
End ClassWhat the Companion SDK takes care of
| Task | Without Companion SDK | With Companion SDK |
|---|---|---|
| NodeSets and dependencies | obtain the files, ship them, and load them in the correct order | embedded; the server class includes all dependencies |
| Namespaces | know the URIs and resolve each namespace with OpcNamespace. | Opc, GetName, and constants for every OpcNodeId |
| Instance of a type | create the node, set TypeDefinitionId, and create each child node yourself with name, namespace, and data type | new Opc creates the child nodes |
| Optional child nodes | decide on and create each one individually | all by default; specific selection through an OpcNodeModel |
| Interfaces | add HasInterface references by hand | automatically from the implemented .NET interfaces |
| Values | find child nodes by name or OpcNodeId and set the value | typed properties, for example Manufacturer. |
| Methods | look up method nodes, IOpc with untyped arguments | override the typed method |
| Attributes from the specification (description, access rights, default values) | copy them by hand | through OpcNodeModel. from the type declaration |
| Client | resolve namespaces and register data types yourself | the client class registers the data types and resolves the namespaces |
Companion SDK in the client
A client also benefits from a Companion SDK. The client class (here OpcMachineryClient) registers the data types of the specification. The client then reads structures and enumerations of the specification directly as .NET types. After connecting, the client class also resolves the namespaces. The constants from OpcMachineryNodes then also apply to the connected server:
using (var client = new OpcMachineryClient("opc.tcp://localhost:4840/")) {
client.Connect();
OpcNodeInfo machines = client.BrowseNode(OpcMachineryNodes.MachinesNode);
foreach (OpcNodeInfo machine in machines.Children())
Console.WriteLine(machine.DisplayName.Value);
}Using client = New OpcMachineryClient("opc.tcp://localhost:4840/")
client.Connect()
Dim machines As OpcNodeInfo = client.BrowseNode(OpcMachineryNodes.MachinesNode)
For Each machine As OpcNodeInfo In machines.Children()
Console.WriteLine(machine.DisplayName.Value)
Next
End UsingTyped node classes such as OpcMachineIdentificationNode are nodes of the server. A client works with the nodes of a companion server through the usual methods for browsing, reading, and writing.

