How-to guide
Manage nodes
Create nodes in the address space of the server, control their visibility, and update them at runtime.
How-to guide
Manage nodes
Create nodes
Types usedOpcServerOpcNodeOpcNodeIdOpcNodeManagerOpcAddressSpaceOpcFolderNodeOpcObjectNodeOpcVariableNodeOpcDataVariableNodeOpcMethodNodeOpcFileNode
An OpcNode defines an element of the address space of the server. This can be a logical folder (OpcFolderNode), a variable (OpcVariableNode), a method (OpcMethodNode), a file (OpcFileNode), and much more. An OpcNodeId uniquely identifies an OpcNode. It consists of a value, the underlying ID, and the index of the namespace to which the node belongs. The value is, for example, a text or a number. A URI (Uniform Resource Identifier) defines the namespace. The node managers of the server determine which namespaces are available.
Each node manager defines at least one namespace. The server uses these namespaces to classify the nodes of a node manager. This way, you can assign each node to a specific node manager. The default namespace of a node manager applies whenever a node does not get another namespace. The nodes that a node manager defines are also called the nodes in the address space of the node manager.
At startup, the server asks its node managers to create their address space, that is, their static nodes. While the server runs, you can add further dynamic nodes to the address space of a node manager and remove them again. You can also create an address space that always stays static without a node manager of your own. To do this, pass the static nodes for the http://{host}/{path}/nodes/ namespace directly to the server. The port is not part of the namespace. Instead of the nodes of the static address space, you can also define custom node managers.
Create a custom address space with a root node for the default namespace http://{host}/{path}/nodes/:
var machineNode = new OpcFolderNode("Machine");
var machineIsRunningNode = new OpcDataVariableNode<bool>(machineNode, "IsRunning");
// Note: An enumerable of nodes can be also passed.
var server = new OpcServer("opc.tcp://localhost:4840/", machineNode);Dim machineNode As New OpcFolderNode("Machine")
Dim machineIsRunningNode = New OpcDataVariableNode(Of Boolean)(machineNode, "IsRunning")
'Note: An enumerable of nodes can be also passed.
Dim server = New OpcServer("opc.tcp://localhost:4840/", machineNode)Create an address space through a method: Alternatively, pass the server a method that creates the address space at startup. The method receives an instance of OpcAddressSpace. Through its AddNode method, it adds the nodes below the ObjectsFolder or another parent node:
var server = new OpcServer("opc.tcp://localhost:4840/", addressSpace => {
var machineNode = new OpcFolderNode("Machine");
var isRunningNode = new OpcDataVariableNode<bool>(machineNode, "IsRunning");
// Adds the node beneath the Objects folder (the root of all server nodes).
addressSpace.AddNode(machineNode);
});Dim server = New OpcServer("opc.tcp://localhost:4840/", Sub(addressSpace)
Dim machineNode = New OpcFolderNode("Machine")
Dim isRunningNode = New OpcDataVariableNode(Of Boolean)(machineNode, "IsRunning")
'Adds the node beneath the Objects folder (the root of all server nodes).
addressSpace.AddNode(machineNode)
End Sub)Build an address space from a configuration: Because the server runs the method at startup, the method can also create the nodes from a configuration, for example from a JSON file, a CSV file, or a database. You do not need a node manager class of your own for this. The following example creates a variable node of the matching type for each entry of a list:
// Name and data type of each variable, e.g. read from a configuration file.
var tags = new Dictionary<string, string> {
{ "Temperature", "double" },
{ "IsRunning", "bool" },
{ "JobName", "string" }
};
var server = new OpcServer("opc.tcp://localhost:4840/", addressSpace => {
var machine = new OpcFolderNode("Machine");
foreach (var tag in tags) {
switch (tag.Value) {
case "double":
new OpcDataVariableNode<double>(machine, tag.Key);
break;
case "bool":
new OpcDataVariableNode<bool>(machine, tag.Key);
break;
default:
new OpcDataVariableNode<string>(machine, tag.Key);
break;
}
}
addressSpace.AddNode(machine);
});'Name and data type of each variable, e.g. read from a configuration file.
Dim tags = New Dictionary(Of String, String) From { _
{"Temperature", "double"}, _
{"IsRunning", "bool"}, _
{"JobName", "string"} _
}
Dim server = New OpcServer("opc.tcp://localhost:4840/", Sub(addressSpace)
Dim machine = New OpcFolderNode("Machine")
For Each tag In tags
Select Case tag.Value
Case "double"
Dim node = New OpcDataVariableNode(Of Double)(machine, tag.Key)
Case "bool"
Dim node = New OpcDataVariableNode(Of Boolean)(machine, tag.Key)
Case Else
Dim node = New OpcDataVariableNode(Of String)(machine, tag.Key)
End Select
Next
addressSpace.AddNode(machine)
End Sub)If the data types should also come from the configuration, the Data types at runtime page describes the procedure.
Define a custom node manager:
public class MyNodeManager : OpcNodeManager
{
public MyNodeManager()
: base("http://mynamespace/")
{
}
}Public Class MyNodeManager
Inherits OpcNodeManager
Public Sub New()
MyBase.New("http://mynamespace/")
End Sub
End ClassCreate a custom address space with a root node through a custom node manager:
protected override IEnumerable<IOpcNode> CreateNodes(OpcNodeReferenceCollection references)
{
// Define custom root node.
var machineNode = new OpcFolderNode(new OpcName("Machine", this.DefaultNamespaceIndex));
// Add custom root node to the Objects-Folder (the root of all server nodes):
references.Add(machineNode, OpcObjectTypes.ObjectsFolder);
// Add custom sub node beneath of the custom root node:
var isMachineRunningNode = new OpcDataVariableNode<bool>(machineNode, "IsRunning");
// Return each custom root node using yield return.
yield return machineNode;
}Protected Overrides Iterator Function CreateNodes(ByVal references As OpcNodeReferenceCollection) As IEnumerable(Of IOpcNode)
'Define custom root node.
Dim machineNode = New OpcFolderNode(New OpcName("Machine", Me.DefaultNamespaceIndex))
'Add custom root node to the Objects-Folder (the root of all server nodes):
references.Add(machineNode, OpcObjectTypes.ObjectsFolder)
'Add custom sub node beneath of the custom root node:
Dim isMachineRunningNode = New OpcDataVariableNode(Of Boolean)(machineNode, "IsRunning")
'Return each custom root node using yield return.
Yield machineNode
End FunctionMake a custom node manager known to the server:
// Note: An enumerable of node managers can be also passed.
var server = new OpcServer("opc.tcp://localhost:4840/", new MyNodeManager());'Note: An enumerable of node managers can be also passed.
Dim server = New OpcServer("opc.tcp://localhost:4840/", New MyNodeManager())Add and remove nodes at runtime: After the server has started, add further nodes through the AddNode method of the node manager and remove them again through its RemoveNode method. The node manager adds a new node below its parent node. The parent node must already be part of the address space:
// The parent node is already part of the address space of the node manager.
var jobNode = new OpcDataVariableNode<string>(machineNode, "Job", value: "JOB0815");
nodeManager.AddNode(jobNode);
// Removes the node from the address space.
nodeManager.RemoveNode(jobNode);'The parent node is already part of the address space of the node manager.
Dim jobNode = New OpcDataVariableNode(Of String)(machineNode, "Job", value:="JOB0815")
nodeManager.AddNode(jobNode)
'Removes the node from the address space.
nodeManager.RemoveNode(jobNode)Restrict the visibility of nodes
Types usedOpcServerIOpcNodeOpcNodeIdOpcNodeManagerOpcObjectNodeOpcDataVariableNode
Not all nodes always need to be visible to all users of the server. In your own node manager, you can restrict the visibility of the nodes by any criteria. Often, restricting by user identity and a specific node is enough. The following simple tree illustrates this:
protected override IEnumerable<IOpcNode> CreateNodes(OpcNodeReferenceCollection references)
{
var machine = new OpcObjectNode(
"Machine",
new OpcDataVariableNode<int>("Speed", value: 123),
new OpcDataVariableNode<string>("Job", value: "JOB0815"));
references.Add(machine, OpcObjectTypes.ObjectsFolder);
yield return machine;
}Protected Overrides Iterator Function CreateNodes(ByVal references As OpcNodeReferenceCollection) As IEnumerable(Of IOpcNode)
Dim machine = New OpcObjectNode( _
"Machine", _
New OpcDataVariableNode(Of Integer)("Speed", value:=123), _
New OpcDataVariableNode(Of String)("Job", value:="JOB0815"))
references.Add(machine, OpcObjectTypes.ObjectsFolder)
Yield machine
End FunctionIf a user may access all nodes except the "Speed" node, the implementation of the IsNodeAccessible method can look like this. Call the base implementation for all other nodes. Among other things, it hides variables whose AccessLevel or UserAccessLevel has the value None:
protected override bool IsNodeAccessible(OpcContext context, OpcNodeId viewId, IOpcNodeInfo node)
{
if (context.Identity.DisplayName == "a")
return true;
if (context.Identity.DisplayName == "b" && node.Name.Value == "Speed")
return false;
return base.IsNodeAccessible(context, viewId, node);
}Protected Overrides Function IsNodeAccessible(ByVal context As OpcContext, ByVal viewId As OpcNodeId, ByVal node As IOpcNodeInfo) As Boolean
If context.Identity.DisplayName = "a" Then
Return True
End If
If context.Identity.DisplayName = "b" AndAlso node.Name.Value = "Speed" Then
Return False
End If
Return MyBase.IsNodeAccessible(context, viewId, node)
End FunctionUpdate nodes
Types usedOpcDataVariableNodeOpcStatusCodeOpcContext
While the server runs, you often have to update nodes to match the underlying system. Simple object, folder, or method nodes change rarely, while variable nodes change regularly. Depending on the information available, you store or update a timestamp and the quality of the value in the variable node in addition to the value itself. The following example outlines how this works:
speedNode.Status.Update(OpcStatusCode.Good);
speedNode.Timestamp = DateTime.UtcNow;
speedNode.Value = 1200;
// Notifies subscribed clients about the changes.
speedNode.ApplyChanges(server.SystemContext);speedNode.Status.Update(OpcStatusCode.Good)
speedNode.Timestamp = DateTime.UtcNow
speedNode.Value = 1200
'Notifies subscribed clients about the changes.
speedNode.ApplyChanges(server.SystemContext)Only when you call ApplyChanges does the server notify client applications about the change of the node, provided they have set up an active subscription for the node and its Value attribute. Outside a node manager, the SystemContext property of the server serves as the context; inside a node manager, its own SystemContext property does. The ApplyChanges(context, recursive: true) overload also applies the changes of all child nodes.
Define namespaces
Types usedOpcServerOpcNamespace
At startup, the server assigns the namespace indexes in the order in which its node managers register their namespaces. Clients that address nodes through a fixed NamespaceIndex (for example configured SCADA or PLC clients) therefore depend on a fixed order. With RegisterNamespace or RegisterNamespaces, you register your own namespaces in a fixed order before startup. The server does not add an already registered namespace again. The methods return instances of OpcNamespace whose Index property contains the assigned index:
using (var server = new OpcServer("opc.tcp://localhost:4840/")) {
OpcNamespace[] namespaces = server.RegisterNamespaces(
"http://example.com/machine",
"http://example.com/plant");
Console.WriteLine("Machine: ns={0}", namespaces[0].Index);
Console.WriteLine("Plant: ns={0}", namespaces[1].Index);
server.Start();
}Using server = New OpcServer("opc.tcp://localhost:4840/")
Dim namespaces As OpcNamespace() = server.RegisterNamespaces( _
"http://example.com/machine", _
"http://example.com/plant")
Console.WriteLine("Machine: ns={0}", namespaces(0).Index)
Console.WriteLine("Plant: ns={0}", namespaces(1).Index)
server.Start()
End UsingBy default, all nodes are in the custom namespace with index two. The namespace with index one is the namespace of the server application: the SDK enters the OpcServer.ApplicationUri property there, provided you do not register any namespaces before you start the server. To manage nodes in the namespace with index one, your own OpcNodeManager must use the value of OpcServer.ApplicationUri as its NamespaceUri.
Multilingual texts
Types usedOpcServerOpcServerGlobalizationOpcText
You can provide the display names, descriptions, and other texts of a node (OpcText) in several languages without creating separate nodes for each language. To do this, the text gets a key in addition to the default text and its language. Through the Globalization property of the server, you store the translations per language for each key (AddResource for one entry, AddResources for several). Specify a specific culture as the language (for example de-DE, not just de):
var temperature = new OpcDataVariableNode<double>("Temperature");
temperature.DisplayName = new OpcText(
"Temperature",
cultureName: "en-US",
key: "Temperature.DisplayName");
temperature.Description = new OpcText(
"Temperature of the machine",
cultureName: "en-US",
key: "Temperature.Description");
using (var server = new OpcServer("opc.tcp://localhost:4840/", temperature)) {
server.Globalization.AddResource(
cultureName: "de-DE",
key: "Temperature.DisplayName",
value: "Temperatur");
server.Globalization.AddResource(
cultureName: "de-DE",
key: "Temperature.Description",
value: "Temperatur der Maschine");
server.Start();
}Dim temperature = New OpcDataVariableNode(Of Double)("Temperature")
temperature.DisplayName = New OpcText( _
"Temperature", _
cultureName:="en-US", _
key:="Temperature.DisplayName")
temperature.Description = New OpcText( _
"Temperature of the machine", _
cultureName:="en-US", _
key:="Temperature.Description")
Using server = New OpcServer("opc.tcp://localhost:4840/", temperature)
server.Globalization.AddResource( _
cultureName:="de-DE", _
key:="Temperature.DisplayName", _
value:="Temperatur")
server.Globalization.AddResource( _
cultureName:="de-DE", _
key:="Temperature.Description", _
value:="Temperatur der Maschine")
server.Start()
End UsingWhen a client reads such a text, the server translates it based on the languages that the client prefers when it creates the session. In the SDK, the client sets these languages through OpcClient.Locales. The Configure the application and session section in the client chapter describes this. If no translation exists for any of the preferred languages, the server returns the default text.
Report changes to the address space
Types usedOpcAutomatismOpcNodeOpcNodeManager
If the address space changes at runtime, for example because a machine or a module is added, the server can notify clients through model change events (GeneralModelChangeEvent). By default, the server does not generate these events. For all nodes outside the namespaces of the OPC Foundation, you turn them on through the static OpcAutomatism.RaiseModelChangeEvents property:
OpcAutomatism.RaiseModelChangeEvents = true;OpcAutomatism.RaiseModelChangeEvents = TrueAfter that, the server reports, for example, a node added at runtime to the clients that have subscribed to the events of the server:
var machine = new OpcObjectNode(machines, "Machine3");
var temperature = new OpcDataVariableNode<double>(machine, "Temperature");
manager.AddNode(server.SystemContext, machine);Dim machine = New OpcObjectNode(machines, "Machine3")
Dim temperature = New OpcDataVariableNode(Of Double)(machine, "Temperature")
manager.AddNode(server.SystemContext, machine)If you change many nodes at once, temporarily suspend the recording of changes for a node with SuspendModelChangeEvents and resume it with ResumeModelChangeEvents. With recursive: true, this also applies to all of its child nodes:
machines.SuspendModelChangeEvents(recursive: true);
foreach (var name in new string[] { "Machine4", "Machine5", "Machine6" })
manager.AddNode(server.SystemContext, new OpcObjectNode(machines, name));
machines.ResumeModelChangeEvents(recursive: true);machines.SuspendModelChangeEvents(recursive:=True)
For Each name In New String() {"Machine4", "Machine5", "Machine6"}
manager.AddNode(server.SystemContext, New OpcObjectNode(machines, name))
Next
machines.ResumeModelChangeEvents(recursive:=True)
