Skip to content

How-to guide

NodeSets

Import information models as NodeSets into the server and implement their nodes.

This content was machine translated and may contain errors. Read in German

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:

csharp
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();
    ...
}
vbnet
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 Using

When 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:

csharp
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);
}
vbnet
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 Using

The 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 missing DefaultEncodingId to 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:

csharp
umatiManager.ImportOptions = OpcNodeSetImportOptions.CompleteDataTypeDefinitionFields
        | OpcNodeSetImportOptions.CreateNonGenericNodeInstances;
vbnet
umatiManager.ImportOptions = OpcNodeSetImportOptions.CompleteDataTypeDefinitionFields _
        Or OpcNodeSetImportOptions.CreateNonGenericNodeInstances

If a custom node manager should import a NodeSet instead, override the ImportNodes method of the OpcNodeManager class:

csharp
protected override IEnumerable<OpcNodeSet> ImportNodes()
{
    yield return OpcNodeSet.Load(@".\umati.xml");
    yield return OpcNodeSet.Load(@".\umati-instances.xml");
}
vbnet
Protected Overrides Iterator Function ImportNodes() As IEnumerable(Of OpcNodeSet)
    Yield OpcNodeSet.Load(".\umati.xml")
    Yield OpcNodeSet.Load(".\umati-instances.xml")
End Function

Connect 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:

csharp
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));
    }
};
vbnet
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 Sub

In 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:

csharp
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.
}
vbnet
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 Sub

For the UMATI NodeSet shown above, the implementation in a custom node manager looks like this:

csharp
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));
        }
    }
}
vbnet
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 Sub

Companion 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 ​

ComponentContent, using Opc.UaFx.Advanced.Machinery as an example
Embedded NodeSetThe NodeSet of the specification is embedded in the assembly; you neither have to obtain it nor ship it.
DependenciesThe server class OpcMachineryServer includes the node managers of all required specifications (DI, IA, and Machinery).
ConstantsOpcMachineryNodes 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 classesA class for each object type, for example OpcMachineIdentificationNode, 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.
InterfacesThe interfaces of the specification are implemented as .NET interfaces. The SDK creates the corresponding HasInterface references automatically when it implements the nodes.
Typed methodsMethods of the specification are implemented as overridable methods with typed parameters. Without an implementation, they throw a NotImplementedException.
Node managerOpcMachineryNodeManager imports the embedded NodeSet, resolves the namespace, and provides the Machines folder.
Client classOpcMachineryClient registers the data types of the specification and its dependencies and resolves the namespaces after connecting, so that the constants from OpcMachineryNodes 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:

csharp
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);
}
vbnet
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 Using

Unless 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:

  1. 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).
  2. Resolve the namespaces of the specifications by their URI to build the OpcNodeId and OpcName of the types and instances.
  3. Create the instance and set its type definition (TypeDefinitionId).
  4. Create each child node yourself with the name, namespace, and data type from the specification.
  5. Add the references to the interfaces of the type (HasInterface) by hand.
csharp
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;
    }
}
vbnet
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 Class

The 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:

csharp
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
    }
}
vbnet
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 Class

With 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:

csharp
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;
    }
}
vbnet
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 Class

What the Companion SDK takes care of ​

TaskWithout Companion SDKWith Companion SDK
NodeSets and dependenciesobtain the files, ship them, and load them in the correct orderembedded; the server class includes all dependencies
Namespacesknow the URIs and resolve each namespace with OpcNamespace.ResolveOpcMachineryNodes.Namespace, GetName, and constants for every OpcNodeId
Instance of a typecreate the node, set TypeDefinitionId, and create each child node yourself with name, namespace, and data typenew OpcMachineIdentificationNode(parent, name) creates the child nodes
Optional child nodesdecide on and create each one individuallyall by default; specific selection through an OpcNodeModel
Interfacesadd HasInterface references by handautomatically from the implemented .NET interfaces
Valuesfind child nodes by name or OpcNodeId and set the valuetyped properties, for example Manufacturer.Value
Methodslook up method nodes, IOpcMethodCommand with untyped argumentsoverride the typed method
Attributes from the specification (description, access rights, default values)copy them by handthrough OpcNodeModel.Of from the type declaration
Clientresolve namespaces and register data types yourselfthe 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:

csharp
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);
}
vbnet
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 Using

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