Client Development Guide & Tutorial


A Client for OPC UA

  1. Add reference to the Opc.UaFx.Advanced Client Namespace:
    Imports Opc.UaFx.Client
  2. Create an instance of the OpcClient class with the address of the Server:
    Dim client = New OpcClient("opc.tcp://localhost:4840/")
  3. Build a connection to the Server and start a session:
    client.Connect()
  4. Your code to interact with the Server:
    'Your code to interact with the server.
  5. Close all sessions before closing the application:
    client.Disconnect()
  6. Using the using block this looks as follows:
    Using client As New OpcClient("opc.tcp://localhost:4840/")
        client.Connect()
        'Your code to interact with the server.
    End Using
  7. Are the Nodes/NodeIds of the Server …

A Client for OPC Classic

  1. Add reference to the Opc.UaFx.Advanced Classic and Client Namespaces:
    Imports Opc.UaFx.Client
    Imports Opc.UaFx.Client.Classic
  2. Create an instance of the OpcClient class with the address of the Server:
    Dim client As New OpcClient("opc.com://localhost:48410/<progId>/<classId>")
  3. Build a connection to the Server and start a session:
    client.Connect()
  4. Your code to interact with the Server:
    'Your code to interact with the server.
  5. Close all sessions before closing the application:
    client.Disconnect()
  6. Using the using block this looks as follows:
    Using client As New OpcClient("opc.com://localhost:48410/<progId>/<classId>")
        client.Connect()
        'Your code to interact with the server.
    End Using
  7. Are the Nodes/NodeIds of the Server …

What describes the address of the server opc.com://localhost:48410/<progId>/<classId> ?

  • opc.com indicates that a connection to an OPC Classic Server is to be used.
  • localhost stands for the name or the IP address of the computer on which the OPC Classic Server is running.
  • 48410 the optional port number of the OPC UA Wrapper Server 1). If this is missing, a port number will be generated based on the <classId>.
  • <progId> is a wildcard, replace it with the ProgID of the DCOM Application Information of the OPC Classic Server,
    e.g. 'OPCManager.DA.XML-DA.Server.DA'.
  • <classId> is a wildcard, replace it with the ClassId (also known as CLSID or AppID) of the DCOM Application Information of the OPC Classic Server,
    e.g. ' {E4EBF7FA-CCAC-4125-A611-EAC4981C00EA} '.

How can I determine the <classId> or <progId> of the server?

  1. Method: OpcClassicDiscoveryClient
    Using discoveryClient As New OpcClassicDiscoveryClient("<host>")
        Dim servers = discoveryClient.DiscoverServers()
     
        For Each server In servers
            Console.WriteLine( _
                    "- {0}, ClassId={1}, ProgId={2}", _
                    server.Name, _
                    server.ClassId, _
                    server.ProgId)
        Next
    End Using
  2. Method: Systeminformation
    Perform the following steps on the OPC Classic Server computer:
    1. AppID using the Component Services
      • Control Panel
      • Administrative Tools
      • Component Services
      • expand 'Component Services'
      • expand 'Computers'
      • expand 'My Computer'
      • expand 'DCOM Config'
      • select '<OPC Classic Server>'
      • open 'Properties'
      • first tab 'General'
      • copy the 'Application ID:' (= AppID)
    2. ProgId using the System Registry
      • 'Windows-Key' + 'R'
      • enter 'regedit'
      • click 'Run'
      • expand the key 'Computer'
      • then the key 'HKEY_LOCAL_MACHINE'
      • expand 'SOFTWARE'
      • expand 'SYSWOW6432Node' (only on 64 Bit Systems else continue)
      • expand 'Classes'
      • expand 'CLSID'
      • expand '<classId>'
      • select 'ProgID'
      • copy the '(Default)' value (= ProgID) in the 'Data' column

Reading Values

The following types are used: OpcClient, OpcNodeId, OpcValue, OpcAttribute and OpcReadNode.

The OpcNodeId of a Node decides on which Node is to be read. When a Node value is read the current value of the value attribute is read by default. The hereby determined OpcValue consists, additionally to the actual value, of a time stamp at which the value was identified at the source of the value (SourceTimestamp) and of a second time stamp at which the value was registered by the Server (ServerTimestamp). If another attribute of the Node shall be read the according OpcAttribute has to be mentioned at the call of ReadNode and the concerning OpcReadNode instance.

  • Read the value of the value attribute of a single Node:
    Dim isRunning As OpcValue = client.ReadNode("ns=2;s=Machine/IsRunning")
  • Read the values of the value attibute of several Nodes:
    Dim commands As OpcReadNode() = New OpcReadNode() { _
        New OpcReadNode("ns=2;s=Machine/Job/Number"), _
        New OpcReadNode("ns=2;s=Machine/Job/Name"), _
        New OpcReadNode("ns=2;s=Machine/Job/Speed") _
    }
     
    Dim job As IEnumerable(Of OpcValue) = client.ReadNodes(commands)
  • Read the value of the DisplayName attribute of a single Node:
    Dim isRunningDisplayName As OpcValue = client.ReadNode("ns=2;s=Machine/IsRunning", OpcAttribute.DisplayName)
  • Read the values of the DisplayName attribute of several Nodes:
    Dim commands As OpcReadNode() = New OpcReadNode() { _
        New OpcReadNode("ns=2;s=Machine/Job/Number", OpcAttribute.DisplayName), _
        New OpcReadNode("ns=2;s=Machine/Job/Name", OpcAttribute.DisplayName), _
        New OpcReadNode("ns=2;s=Machine/Job/Speed", OpcAttribute.DisplayName) _
    }
     
    Dim jobDisplayNames As IEnumerable(Of OpcValue) = client.ReadNodes(commands)

Writing Values

The OpcNodeId of a Node decides which Node to write. When a Node value is written, the current value of the value attribute is written by default. The hereby set OpcValue automatically receives the latest time stamp as the time stamp of the source (SourceTimestamp). If another attribute of the Node shall be written the according OpcAttribute has to be mentioned at the call of the WriteNode or at the concerning OpcWriteNode instance.

  • Write the value of a single Node:
    Dim result As OpcStatus = client.WriteNode("ns=2;s=Machine/Job/Cancel", True)
  • Write the values of several Nodes:
    Dim commands As OpcWriteNode() = New OpcWriteNode() { _
        New OpcWriteNode("ns=2;s=Machine/Job/Number", "0002"), _
        New OpcWriteNode("ns=2;s=Machine/Job/Name", "MAN_F01_78910"), _
        New OpcWriteNode("ns=2;s=Machine/Job/Speed", 1220.5) _
    }
     
    Dim results As OpcStatusCollection = client.WriteNodes(commands)
  • Write the value of the DisplayName attribute of a single Node:
    client.WriteNode("ns=2;s=Machine/IsRunning", OpcAttribute.DisplayName, "IsActive")
  • Write the values of the DisplayName attribute of several Nodes:
    Dim commands As OpcWriteNode() = New OpcWriteNode() { _
        New OpcWriteNode("ns=2;s=Machine/Job/Number", OpcAttribute.DisplayName, "Serial"), _
        New OpcWriteNode("ns=2;s=Machine/Job/Name", OpcAttribute.DisplayName, "Description"), _
        New OpcWriteNode("ns=2;s=Machine/Job/Speed", OpcAttribute.DisplayName, "Rotations per Second") _
    }
     
    Dim results As OpcStatusCollection = client.WriteNodes(commands)

Processing Values

The following types are used here: OpcClient, OpcNodeId, OpcValue, OpcStatus and OpcStatusCollection.

The ReadNode methods always provide an OpcValue instance, while the ReadNodes methods provide a list of OpcValue instances (one OpcValue per read Node). The actual value read is in the Value property of the OpcValue instances. The result of the read request can be checked via the Status property. The timestamp at which the read value has been detected at the sourcecan be retrieved via the SourceTimestamp property. Correspondingly, the timestamp at which the read value was detected by the Server can be retrieved via the ServerTimestamp property.

  • Read the value of a single Node:
    Dim value As OpcValue = client.ReadNode("ns=2;s=Machine/Job/Speed")
  • Chech the result of the read request:
    If value.Status.IsGood Then
        'Your code to operate on the value.
    End If
  • Retrieve the scalar value of the OpcValue instance:
    Dim intValue As Integer = CInt(value.Value)
  • Retrieve the array value of the OpcValue instance:
    Dim intValues As Integer() = CType(value.Value, Integer())

The WriteNode methods always provide an OpcStatus instance, while the WriteNodes methods provide an OpcStatusCollection instance (that contains an OpcStatus for every written Node). The result of the write request can thereby be checkt via the properties of the OpcStatus instance(s).

  • Write the scalar value of a single Node:
    Dim status As OpcStatus = client.WriteNode("ns=2;s=Machine/Job/Speed", 1200)
  • Write the array value of a single Node:
    Dim values As Integer() = New Integer(2) {1200, 1350, 1780}
    Dim status As OpcStatus = client.WriteNode("ns=2;s=Machine/Job/Speeds", values)
  • Check the result of a write request:
    If Not status.IsGood Then
        'Your code to handle a failed write operation.
    End If

By using the individual steps to prepare the processing of scalar values and array values, the array value of a Variable Node can be modified as follows:

Using client As New OpcClient("opc.tcp://localhost:4840")
    client.Connect()
    Dim arrayValue As OpcValue = client.ReadNode("ns=2;s=Machine/Job/Speeds")
 
    If arrayValue.Status.IsGood Then
        Dim intArrayValue As Integer() = CType(arrayValue.Value, Integer())
 
        intArrayValue(2) = 100
        intArrayValue(4) = 200
        intArrayValue(9) = 300
 
        Dim status As OpcStatus = client.WriteNode("ns=2;s=Machine/Job/Speeds", intArrayValue)
 
        If Not status.IsGood Then _
            Console.WriteLine("Failed to write array value!")
    End If
End Using

Which Nodes does the Server have?

The following types are used here: OpcClient, OpcNodeId, OpcNodeInfo, OpcAttribute, OpcAttributeInfo and OpcObjectTypes.

Starting from a Server whose Address-Space (all available nodes) are still (almost) unknown, it is advisable to inspect the provided Nodes of the Server. Either a graphical OPC UA client like the

OPC Watch

can be used or the Address-Space of the Server can be examined manually.

When using the class OpcObjectTypes already predefined Server-Nodes can also be examined by browsing. The “root” of all Nodes of the Server represents the Default-Node called “ObjectsFolder”. When browsing is started at the “ObjectsFolder”-Node, the entire Address-Space of the Server can be determined:

Dim node = client.BrowseNode(OpcObjectTypes.ObjectsFolder)
Browse(node)
...
Private Sub Browse(ByVal node As OpcNodeInfo, ByVal Optional level As Integer = 0)
    Console.WriteLine("{0}{1}({2})", _
            New String("."c, level * 4), _
            node.Attribute(OpcAttribute.DisplayName).Value, _
            node.NodeId)
 
    level += 1
 
    For Each childNode In node.Children()
        Browse(childNode, level)
    Next
End Sub

The output via the code snippet shown contains, among other things (in parentheses) the Node-IDs of the Nodes of the Server. Based on these NodeId's the Nodes of the Server can be addressed directly. Simply pass the NodeId as a string (in double quotes) to the corresponding method of the OpcClient class. For example, what it looks like when reading a node value is shown in the section Reading Values.

Inspecting Node by Node

Browsing in the OPC UA can be compared to .NET Reflections. Through browsing it is therefore possible to dynamically determine and examine the entire “Address Space” of a Server. This includes all Nodes, their references towards each other and their Node Types. Browsing is introduced through the OpcNodeId of the Node on which the browsing procedure shall be started. Coming from the hereby retrieved OpcNodeInfo, browsing can be continued on Child, Parent or Attribute Level.

If a Node is a method-depicting Node, then browsing provides an OpcMethodNodeInfo instance by the help of which Input / Output arguments of the method can be examined.

  1. Determine the OpcNodeInfo of the desired Node:
    Dim machineNode As OpcNodeInfo = client.BrowseNode("ns=2;s=Machine")
  2. Use the Child or Children method to browse the Child-Node:
    Dim jobNode As OpcNodeInfo = machineNode.Child("Job")
     
    For Each childNode In machineNode.Children()
        'Your code to operate on each child node.
    Next
  3. Use the Attribute or Attributes method to browse the attributes:
    Dim displayName As OpcAttributeInfo = machineNode.Attribute(OpcAttribute.DisplayName)
     
    For Each attribute In machineNode.Attributes()
        'Your code to operate on each attribute.
    Next
  4. In case a Node depicts a method, the Node can be visualized and examined like this:
    If childNode.Category = OpcNodeCategory.Method Then
        Dim methodNode = CType(childNode, OpcMethodNodeInfo)
     
        For Each argument In methodNode.GetInputArguments()
            'Your code to operate on each argument.
        Next
    End If

High-Speed Browsing

If a Server provides a very extensive “Address Space”, browsing through all nodes is often a bit slow, especially if the large amount of node information also has to be transported via a quite weak hardware or network configuration. The additional load on the Server also leads to a loss of performance if Clients go through the entire node tree node by node.

To optimize this situation, the SDK offers the option of defining how many nodes and node levels should be examined at the same time. The OpcBrowseNodeDegree enumeration is used for this. Depending on the number of nodes required at the same time, the corresponding value of the enumeration is to be specified via the Degree property or in the constructor of the OpcBrowseNode class. There exists also the option of reducing the user data to a minimum or only to the really relevant data when browsing. The OpcBrowseOptions enumeration offers various options for this. It should be noted that at least the ReferenceTypeId must be part of the node information. If you want to receive additional node information, such as the DisplayName, then the DisplayName must also be included (when using the Options property).

Browsing can be parameterized and thus further optimized using the following properties:

'Create a browse command to browse all hierarchical references.
Dim browse = New OpcBrowseNode( _
        nodeId:=OpcNodeId.Parse("ns=2;s=Machine"), _
        degree:=OpcBrowseNodeDegree.Generation)
 
'Create a browse command to browse specific types of references.
Dim browse = New OpcBrowseNode( _
        nodeId:=OpcNodeId.Parse("ns=2;s=Machine"), _
        degree:=OpcBrowseNodeDegree.Generation, _
        referenceTypes:={ _
            OpcReferenceType.Organizes, _
            OpcReferenceType.HasComponent, _
            OpcReferenceType.HasProperty _
        })
 
'Reduce browsing to the smallest possible amount of data.
browse.Options = OpcBrowseOptions.IncludeReferenceTypeId _
        Or OpcBrowseOptions.IncludeBrowseName
 
Dim node = client.BrowseNode(browse)
 
For Each childNode In node.Children()
    'Continue recursively...
Next

It should also be noted that if nodes that only have a certain relationship to one another (which is expressed using ReferenceTypes) are to be examined, browsing can also be further optimized here. The node references relevant for browsing can also be used as parameters for the browse operation (see the code snippet above) so that only the nodes that use one of the specified ReferenceTypes are visited.

High-Speed Browsing - Details

If a Server provides a very extensive “Address Space”, browsing through all nodes is often a bit slow, especially if the large amount of node information also has to be transported via a quite weak hardware or network configuration. The additional load on the Server also leads to a loss of performance if Clients go through the entire node tree node by node.

In such cases it is important to keep the workload for the Server and the entire communication between Client and Server as lean and as short as possible. For this purpose, OPC UA offers various mechanisms with which a Client can signal to the Server that many more nodes than just one level are being examined. The Server then has the option to prepare the information and communicate it to the Client in packets. This therefore reduces further internal reprocessing of the node tree on the Server side, always on request from the Client. In addition, a Client can, with the appropriate logic, query further nodes in advance from the Server, of which it is known that their node information and that of their children and their children-children (and so on) would also be processed.

The SDK offers various parameters for optimizing the browse operations, which can be used to determine the behavior when browsing. Depending on the browsing process, the SDK uses the parameters to decide which nodes with which information should be prepared in advance on the Server side for the current browse process. The SDK then automatically retrieves these nodes during further browsing, stores them in the memory in advance for the further browsing process and then delivers them from the internal cache instead from the Server.

A value of the OpcBrowseNodeDegree enumeration is used to control which nodes are to be automatically requested by the Server at the same time. The value Self corresponds to the standard behavior in which only the children of the current node are examined. In the tree on the left, browsing is started at node A1. As a result, the Server only delivers the nodes directly below node A1, i.e. the nodes A1-B1 and A1-B2. The nodes of all other subtrees, including those of the nodes next to the node A1, at which the browse operation was started, are not part of the browse operation.

With the value Sibling, all children of the sibling nodes are retrieved from the Server at the same time when the child nodes of the current node are examined. In this case, when the browse operation starts at node A1, the SDK not only determines the direct child nodes of node A1, but also those of all nodes that are siblings of A1. This means that the subtrees from A2 to An are also determined during the browse operation. The SDK temporarily stores the node information obtained in this way and (again) retrieves it from the memory instead from the Server as soon as the developer requires the subtree of e.g. Node A2. In the picture on the right you can see that a browse operation - starting from node A1 - behaves as if you were browsing nodes A1 to An at the same time.

If the value Generation is used, all nodes of the same generation are examined at the same time, which means that all nodes of the same level depth are examined. Specifically, this means, as shown in the tree on the left, that if a browse operation is started at the node A1-B1, not only its subtree (as when browsing with OpcBrowseNodeDegree.Self), but also the subtrees of his siblings (as when browsing with OpcBrowseNodeDegree.Sibling) as well as the subtrees of his cousins of the nth degree can also be called up. In this way, browsing behaves as if you were browsing the nodes A1-B1, A1-B2, A2-B1 and A2-B2 at the same time. Here, too, the node information of the subtrees is cached and made available to the developer if required.

It should be noted that the use of the individual OpcBrowseNodeDegree values is always associated with a little caution. If, for example, a Server provides a relatively “flat address space” that contains a large number of nodes per subtree, the “Generation” option can lead to relatively long browse operations, which in turn can be very memory-intensive. The same applies to the Self option, especially if one has a large number of small subtrees on the same level, which in turn also contain many other child nodes.

The general recommendation is therefore that the developer either knows the complexity of the Server in advance and selects the level of browse operations based on this, or in the case of unknown/changing Servers by choosing the level Sibling or Generation the node information shall be reduced via the Options property. Any further necessary node information can then be called up by the developer using a separate browse operation.

Subscription Creation

Subscriptions in the OPC UA can be compared to subscriptions to one or more journals in the bundle. Instead of magazines, however, Node Events (= in the OPC UA: Monitored Item) or changes to the Node Values (= in the OPC UA: Monitored Item) are subscribed. For each subscription, it is possible to specify in which interval the notifications (OpcNotification instances) of the monitored items should be published (OpcSubscription.PublishingInterval). Which Node to subscribe to is determined by the OpcNodeId of the Node. By default, the Value-Attribute is monitored. If another attribute of the Node is to be monitored, the corresponding OpcAttribute have to be specified when calling SubscribeDataChange or when using a respective OpcSubscribeDataChange instance.

  • Subscribe to notifications about changes to the Node Value:
    Dim subscription As OpcSubscription = client.SubscribeDataChange( _
            "ns=2;s=Machine/IsRunning", _
            AddressOf HandleDataChanged)
  • Subscribe to notifications about changes to multiple Node Values:
    Dim commands As OpcSubscribeDataChange() = New OpcSubscribeDataChange() { _
        New OpcSubscribeDataChange("ns=2;s=Machine/IsRunning", AddressOf HandleDataChanged), _
        New OpcSubscribeDataChange("ns=2;s=Machine/Job/Speed", AddressOf HandleDataChanged) _
    }
     
    Dim subscription As OpcSubscription = client.SubscribeNodes(commands)
  • Handle notifications of changes to Node Values:
    Private Shared Sub HandleDataChanged( _
            ByVal sender As Object, _
            ByVal e As OpcDataChangeReceivedEventArgs)
        'Your code to execute on each data change.
        'The 'sender' variable contains the OpcMonitoredItem with the NodeId.
        Dim item As OpcMonitoredItem = CType(sender, OpcMonitoredItem)
     
        Console.WriteLine( _
                "Data Change from NodeId '{0}': {1}", _
                item.NodeId, _
                e.Item.Value)
    End Sub
  • Subscribe to notifications about Node Events:
    Dim subscription As OpcSubscription = client.SubscribeEvent( _
            "ns=2;s=Machine", _
            AddressOf HandleEvent)
  • Subscribe to notifications about Node Events of multiple Nodes:
    Dim commands As OpcSubscribeEvent() = New OpcSubscribeEvent() { _
        New OpcSubscribeEvent("ns=2;s=Machine", AddressOf HandleEvent), _
        New OpcSubscribeEvent("ns=2;s=Machine/Job", AddressOf HandleEvent) _
    }
     
    Dim subscription As OpcSubscription = client.SubscribeNodes(commands)
  • Handle notifications about Node Events:
    Private Sub HandleEvent(ByVal sender As Object, ByVal e As OpcEventReceivedEventArgs)
        'Your code to execute on each event raise.
    End Sub
  • Configuration of the OpcSubscription:
    subscription.PublishingInterval = 2000
     
    'Always call apply changes after modifying the subscription; otherwise
    'the server will not know the new subscription configuration.
    subscription.ApplyChanges()
  • Subscribe to notifications of changes to multiple Node Values through a single subscription and set custom values of the Tag property of the OpcMonitoredItem:
    Dim nodeIds As String() = { _
        "ns=2;s=Machine/IsRunning", _
        "ns=2;s=Machine/Job/Speed", _
        "ns=2;s=Machine/Diagnostics" _
    }
     
    'Create an (empty) subscription to which we will addd OpcMonitoredItems.
    Dim subscription As OpcSubscription = client.SubscribeNodes()
     
    For index As Integer = 0 To nodeIds.Length - 1
        'Create an OpcMonitoredItem for the NodeId.
        Dim item = New OpcMonitoredItem(nodeIds(index), OpcAttribute.Value)
        AddHandler item.DataChangeReceived, AddressOf HandleDataChanged
     
        'You can set your own values on the "Tag" property
        'that allows you to identify the source later.
        item.Tag = index
     
        'Set a custom sampling interval on the 
        'monitored item.
        item.SamplingInterval = 200
     
        'Add the item to the subscription.
        subscription.AddMonitoredItem(item)
    Next
     
    'After adding the items (or configuring the subscription), apply the changes.
    subscription.ApplyChanges()

Handler:

Private Shared Sub HandleDataChanged( _
        ByVal sender As Object, _
        ByVal e As OpcDataChangeReceivedEventArgs)
    'The tag property contains the previously set value.
    Dim item As OpcMonitoredItem = CType(sender, OpcMonitoredItem)
 
    Console.WriteLine( _
        "Data Change from Index {0}: {1}", _
        item.Tag,
        e.Item.Value)
End Sub

Subscription Filtering

A Client application is notified through a once created subscription only about changes of the Status or of the Value of a Node, by default. However, for a Server to notify the Client about changes to the Node according to certain policies, it is possible to setup a trigger of a notification to evaluate. For this, the desired Trigger is determined via a value of the OpcDataChangeTrigger enumeration. The following examples subscribe to notifications about changes in the Status or the Value or the Timestamp of a Node.

  • Subscribe to notifications about changes to the Node Value and Node Timestamps:
    Dim subscription As OpcSubscription = client.SubscribeDataChange( _
            "ns=2;s=Machine/IsRunning", _
            OpcDataChangeTrigger.StatusValueTimestamp, _
            AddressOf HandleDataChanged)
  • Subscribe to notifications about changes to multiple Node Values and Node Timestamps:
    Dim commands As OpcSubscribeDataChange() = New OpcSubscribeDataChange() { _
        New OpcSubscribeDataChange( _
                "ns=2;s=Machine/IsRunning", _
                OpcDataChangeTrigger.StatusValueTimestamp, _
                AddressOf HandleDataChanged), _
        New OpcSubscribeDataChange( _
                "ns=2;s=Machine/Job/Speed", _
                OpcDataChangeTrigger.StatusValueTimestamp, _
                AddressOf HandleDataChanged) _
    }
     
    Dim subscription As OpcSubscription = client.SubscribeNodes(commands)
  • Handle notifications of changes to Node Values and Node Timestamps:
    Private Shared Sub HandleDataChanged( _
            ByVal sender As Object, _
            ByVal e As OpcDataChangeReceivedEventArgs)
        'Your code to execute on each data change.
        'The 'sender' variable contains the OpcMonitoredItem with the NodeId.
        Dim item As OpcMonitoredItem = CType(sender, OpcMonitoredItem)
     
        Console.WriteLine( _
                "Data Change from NodeId '{0}': {1} at {2}", _
                item.NodeId, _
                e.Item.Value, _
                e.Item.Value.SourceTimestamp)
    End Sub

The used OpcDataChangeTrigger-Value of the preceding samples can be defined using an instance of the OpcDataChangeFilter class as well. Doing so enables the reuse of the once defined filter within multiple subscriptions and monitored items:

Dim filter As OpcDataChangeFilter = New OpcDataChangeFilter()
filter.Trigger = OpcDataChangeTrigger.StatusValueTimestamp
 
Dim subscriptionA As OpcSubscription = client.SubscribeDataChange( _
        "ns=2;s=Machine/IsRunning", _
        filter, _
        AddressOf HandleDataChanged)
 
'or
 
Dim commands As OpcSubscribeDataChange() = New OpcSubscribeDataChange() { _
    New OpcSubscribeDataChange( _
            "ns=2;s=Machine/IsRunning", _
            filter, _
            AddressOf HandleDataChanged), _
    New OpcSubscribeDataChange( _
            "ns=2;s=Machine/Job/Speed", _
            filter, _
            HandleDataChanged) _
}
 
Dim subscriptionB As OpcSubscription = client.SubscribeNodes(commands)

In the following sections it is assumed that the server provides a node (via the NodeId “ns=2;s=Machine/Operator”), which makes use of the (fictitious) data type “StaffType” ,
The structure of the data type is defined as follows:

StaffType
  .Name : string
  .ID : long
  .Shift : ShiftInfoType
    .Name : string
    .Elapsed : DateTime
    .Remaining : int

Simplest Access

The following types are used here: OpcClient, OpcNodeId, OpcValue and OpcDataObject.

For easy access to values of variable nodes with structured data, the framework supports the use of the keyword dynamic. Accesses to variables that are declared using dynamic are evaluated by .NET at runtime. This means that the data of a structured data type can be accessed without prior explicit implementation of a .NET type. Such an access could look like this:

client.UseDynamic = True
client.Connect()
 
Dim staff = client.ReadNode("ns=2;s=Machine/Operator").Value
 
'Access the 'Name' and 'ID' field of the data without to declare the data type itself.
'Just use the field names known as they would be defined in a .NET Type.
Console.WriteLine("Name: {0}", staff.Name)
Console.WriteLine("Staff ID: {0}", staff.ID)
 
'Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name)
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed)
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining)
 
'Change Shift
staff.Name = "John"
staff.ID = 4242
staff.Shift.Name = "Swing Shift"
 
client.WriteNode("ns=2;s=Machine/Operator", staff)

Name-based Access

The following types are used here: OpcClient, OpcNodeId, OpcValue, OpcDataObject and OpcDataField.

For name-based access to values of variable nodes with structured data, the OpcDataObject class can be used directly. If the names of the fields of the data type are known, these can be accessed in the following way:

client.UseDynamic = True
client.Connect()
 
Dim staff As OpcDataObject = client.ReadNode("ns=2;s=Machine/Operator").As(Of OpcDataObject)()
 
'Access the 'Name' and 'ID' field of the data without to declare the data type itself.
'Just use the field names known as the 'key' to access the according field value.
Console.WriteLine("Name: {0}", staff("Name").Value)
Console.WriteLine("Staff ID: {0}", staff("ID").Value)
 
'Continue accessing subsequently used data types using the OpcDataObject as before.
Dim shift As OpcDataObject = CType(staff("Shift").Value, OpcDataObject)
 
Console.WriteLine("Shift: {0}", shift("Name").Value)
Console.WriteLine("- Time Elapsed: {0}", shift("Elapsed").Value)
Console.WriteLine("- Jobs Remaining: {0}", shift("Remaining").Value)
 
'Change Shift
staff("Name").Value = "John"
staff("ID").Value = 4242
shift("Name").Value = "Swing Shift"
 
client.WriteNode("ns=2;s=Machine/Operator", staff)

Dynamic Access

The following types are used here: OpcClient, OpcNodeSet, OpcAutomatism and OpcDataTypeSystem.

For the “simples access” (see Simplest Access) via dynamic (in Visual Basic not typed using Dim) as well as the name-based access (see Name-based Access) the OpcClient needs information about the data types provided by the server. This information determines the OpcClient class automatically during Connect() from the server if the property UseDynamic either on the OpcClient or the static class OpcAutomatism is set to the value true before connecting.

By enabling the feature, the OpcClient will load the necessary type information from the server so that it can be accessed following the call to Connect() without explicit coding or shown in the previous sections.

If a UANodeSet.xml or a XML file with the description of the server is available (an XML file whose content starts with UANodeSet ), this file can also be loaded into the client application. The OpcClient can then retrieve the information from the NodeSet and does not need to retrieve the type information when connecting. This could then look like this:

Using Dim client As New OpcClient("opc.tcp://localhost:4840")
    client.NodeSet = OpcNodeSet.Load("..\Resources\MyServersNodeSet.xml")
    client.UseDynamic = True
 
    client.Connect()
    Dim staff = client.ReadNode("ns=2;s=Machine/Operator").Value
 
    Console.WriteLine("Name: {0}", staff.Name)
    Console.WriteLine("Staff ID: {0}", staff.ID)
End Using

Note that the OpcClient class when calling GetDataTypeSystem() after setting a NodeSet provides an instance of the OpcDataTypeSystem class that describes the type system described in the NodeSet. If, on the other hand, no NodeSet is set using the NodeSet property of the OpcClient class, calling GetDataTypeSystem() returns a OpcDataTypeSystem instance representing the server's type system which was determined when calling Connect ().

Typed Access

The following types are used here: OpcClient, OpcNodeId, OpcDataTypeAttribute and OpcDataTypeEncodingAttribute.

For typed access, .NET types are defined in the client application as they are defined in the server. All required metadata is provided via attributes. Definition of .NET types for structured data types:

<OpcDataType("ns=2;s=StaffType")>
<OpcDataTypeEncoding("ns=2;s=StaffType-Binary")>
Public Class Staff
    Public Property Name As String
    Public Property ID As Integer
    Public Property Shift As ShiftInfo
End Class
 
 
<OpcDataType("ns=2;s=ShiftInfoType")>
<OpcDataTypeEncoding("ns=2;s=ShiftInfoType-Binary")>
Public Class ShiftInfo
    Public Property Name As String
    Public Property Elapsed As DateTime
    Public Property Remaining As Byte
End Class

After the definition of the .NET type for a structured data type (on the client-side) defined by the server, this can be used as follows:

client.Connect()
Dim staff As Staff = client.ReadNode("ns=2;s=Machine/Operator").As(Of Staff)()
 
'Access the 'Name' and 'ID' field of the data with the declared the data type.
Console.WriteLine("Name: {0}", staff.Name)
Console.WriteLine("Staff ID: {0}", staff.ID)
 
'Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name)
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed)
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining)
 
'Change Shift
staff.Name = "John"
staff.ID = 4242
staff.Shift.Name = "Swing Shift"
 
client.WriteNode("ns=2;s=Machine/Operator", staff)

Generate Data Types

If the typed access (see Typed Access) is to be used, there is the option to generate either only certain or all data types of an OPC UA server via the OPC Watch.

If the typed access (see Typed Access) is to be used, there is the option to generate either only certain or all data types of an OPC UA server via the OPC Watch.

Generation using a Server

To generate a single data type implemented in .NET, perform the following steps:

  1. Open OPC Watch
  2. Create a new connection (+) and configure it (if necessary)
  3. Connect to the server (plug icon)
  4. Select a node…
    1. either a variable-node with a structured data type as its value
    2. or a DataType-node under /Types/DataTypes/BaseDataType/Structure or /Enumeration
  5. Right click on this node
  6. Click “Generate DataType”
  7. Paste the code into the application, done!

All data types can be generated either in a code file (*.cs) or assembly file (*.dll). To do this, follow these steps:

  1. Open OPC Watch
  2. Create a new connection (+) and configure it (if necessary)
  3. Connect to the server (plug icon)
  4. Select the Server-node “opc.tcp://…”
  5. Right click on this node
  6. Click “Generate Models”
  7. Select the desired file type in the dialog
  8. Click “Save”
  9. Add the file to the project, done!

Generation using a NodeSet

  1. Open OPC Watch
  2. Click the first icon in the upper right corner of the application
  3. Open the NodeSet file (usually a XML file of a Companion Specification)
  4. NodeSet is loaded and then a “Save as …” dialog is displayed
  5. Select the desired file type in the dialog
  6. Click “Save”
  7. Add the file to the project, done!

Define Data Types

As an alternative to generating the data type as a .NET code or .NET assembly (see Generate Data Types), these can also be defined manually. To do this, implement the type as defined by the server. This means that the .NET Type (regardless of structure or class) must provide the fields of the structured data type - in terms of their type - in exactly the same order. All other metadata is provided via corresponding attributes:

<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
        "<NodeId of Binary Encoding Node>", _
        NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
Friend Structure MyDataType
    Public FieldA As Short
    Public FieldB As Integer
    Public FieldC As String
    ...
End Structure

The information required for the definition can be obtained either via the OPC UA Server manual, the responsible PLC developer or via the OpcClient class. To get the necessary information via the OpcClient class, you can examine the variable node - which uses the structured data types - as follows:

Dim node As OpcNodeInfo = client.BrowseNode("ns=2;s=Machine/Operator")
Dim variableNode As OpcVariableNodeInfo = TryCast(node, OpcVariableNodeInfo)
 
If variableNode IsNot Nothing Then
    Dim dataTypeId As OpcNodeId = variableNode.DataTypeId
    Dim dataType As OpcDataTypeInfo = client.GetDataTypeSystem().GetType(dataTypeId)
 
    Console.WriteLine(dataType.TypeId)
    Console.WriteLine(dataType.Encoding)
 
    Console.WriteLine(dataType.Name)
 
    For Each field As OpcDataFieldInfo In dataType.GetFields()
        Console.WriteLine(".{0} : {1}", field.Name, field.FieldType)
    Next
 
    Console.WriteLine()
    Console.WriteLine("Data Type Attributes:")
    Console.WriteLine( _
            vbTab & "[OpcDataType(""{0}"")]", _
            dataType.TypeId.ToString(OpcNodeIdFormat.Foundation))
    Console.WriteLine( _
            vbTab & "[OpcDataTypeEncoding(""{0}"", NamespaceUri = ""{1}"")]", _
            dataType.Encoding.Id.ToString(OpcNodeIdFormat.Foundation), _
            dataType.Encoding.Namespace.Value)
End If

Data Types with optional Fields

In order to reduce the amount of data transported, it is possible to “mark” certain fields of a structured data type as existent or missing in the data stream depending on certain conditions or on the value of another field within the data structure. Thus, to “mark” that a field is available, either the value of another field or a single bit within the EncodingMask (a field coded as a preamble before the data in the stream) is used. The size of the EncodingMask is specified in number of bytes in the 'OpcDataTypeEncodingMaskAttribute'; if the 'Size' property is not explicitly set, its value (if 'OpcEncodingMaskKind.Auto' is used) will be the smallest number of bytes needed (based on the number of optional fields).

The following options are available for defining the optional fields:

<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
        "<NodeId of Binary Encoding Node>", _
        NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
<OpcDataTypeEncodingMask(OpcEncodingMaskKind.Auto)>
Friend Structure MyDataTypeWithOptionalFields
    Public FieldA As Short
    Public FieldB As Integer
    Public FieldC As String
 
    'Nullables are treat as optional fields by default.
    'Existence-Indicator-Bit is Bit0 in the encoding mask.
    Public OptionalField1 As UInteger?
 
    'Existence-Indicator-Bit is Bit1 (the next unused bit) in the encoding mask.
    <OpcDataTypeMemberSwitch>
    Public OptionalField2 As Integer
 
    'Existence-Indicator-Bit is Bit3 (bit 2 is unused) in the encoding mask.
    <OpcDataTypeMemberSwitch(3)>
    Public OptionalField3 As Byte
 
    Public FieldD As Boolean
 
    ''OptionalField4' exists only if the value of 'FieldD' is equals 'true'.
    <OpcDataTypeMemberSwitch("FieldD")>
    Public OptionalField4 As String
 
    Public FieldE As Integer
 
    ''OptionalField5' exists only if the value of 'FieldE' is greater than '42'.
    <OpcDataTypeMemberSwitch("FieldE", 42, OpcMemberSwitchOperator.GreaterThan)>
    Public OptionalField5 As String
End Structure

The following types are used here: OpcClient, OpcNodeId, IOpcNodeHistoryNavigator, OpcHistoryValue and OpcAggregateType.

According to OPC UA specification every Node of the category Variable supports the historical recording of the values from its Value Attribute. Hereby the new value is saved together with the time stamp of the Value Attribute at every change of value of the Value Attribute. These pairs consisting of value and timestamp are called historical data. The Server itself decides on where to save the data. However, the Client can detect via the IsHistorizing Attribute of the Node, if the Server provides historical data for a Node and / or historically saves value changes. The OpcNodeId of the Node determines from which Node the historical data shall be accessed. Hereby the Client can read, update, replace, delete and create historical data. Mostly, historical data is read by the Client. Processing all historical values, independent from reading with or without navigator, is not necessary.

In order to read historical data the Client can:

  • read all values within an open time frame (= non-defined StartTime or EndTime)
    • read all values from a particular timestamp forward (= StartTime):
      Dim startTime = New DateTime(2017, 2, 16, 10, 0, 0)
      Dim history = client.ReadNodeHistory( _
              startTime, Nothing, "ns=2;s=Machine/Job/Speed")
    • read all values up until a particular timestamp (= EndTime):
      Dim endTime = New DateTime(2017, 2, 16, 15, 0, 0)
      Dim history = client.ReadNodeHistory( _
              Nothing, endTime, "ns=2;s=Machine/Job/Speed")
  • read all values within a closed time window (= defined StartTime and EndTime):
    Dim startTime = New DateTime(2017, 2, 16, 10, 0, 0)
    Dim endTime = New DateTime(2017, 2, 16, 15, 0, 0)
     
    Dim history = client.ReadNodeHistory( _
            startTime, endTime, "ns=2;s=Machine/Job/Speed")
  • The values are being processed via an instance that implements the IEnumerable interface:
    For Each value In history
        Console.WriteLine( _
                "{0}: {1}",
                value.Timestamp,
                value)
    Next

In order to read historical data pagewise (= only a particular number of values is retrieved from the Server) the Client can:

  • read a particular number of values per page:
    Dim historyNavigator = client.ReadNodeHistory( _
            10, "ns=2;s=Machine/Job/Speed")
  • read a particular number of values per page within an open time frame (= undefined StartTime or EndTime)
    • read a particular number of values per page from a particular timestamp forward (= StartTime):
      Dim startTime = New DateTime(2017, 2, 16, 15, 0, 0)
      Dim historyNavigator = client.ReadNodeHistory( _
              startTime, 10, "ns=2;s=Machine/Job/Speed")
    • read a particular number of values per page up until a particular timestamp (= EndTime):
      Dim endTime = New DateTime(2017, 2, 16, 15, 0, 0)
      Dim historyNavigator = client.ReadNodeHistory( _
              Nothing, endTime, 10, "ns=2;s=Machine/Job/Speed")
  • read a particular number of values per page within a closed time frame (= defined StartTime and EndTime):
    Dim startTime = New DateTime(2017, 2, 16, 10, 0, 0)
    Dim endTime = New DateTime(2017, 2, 16, 15, 0, 0)
     
    Dim historyNavigator = client.ReadNodeHistory( _
            startTime, endTime, 10, "ns=2;s=Machine/Job/Speed")
  • The values are then processed via an instance that implements the IOpcNodeHistoryNavigator interface:
    Do
        For Each value In historyNavigator
            Console.WriteLine( _
                    "{0}: {1}", _
                    value.Timestamp, _
                    value)
        Next
    Loop While historyNavigator.MoveNextPage()
     
    historyNavigator.Close()
  • Always ensure that the Close method of the IOpcNodeHistoryNavigator instance is called. This is necessary in order for the Server to be able to dispose of the historical data buffered for the request afterwards. As an alternative to the explicit call of the Close method the navigator can also be used in a using block:
    Using historyNavigator
        Do
            For Each value In historyNavigator
                Console.WriteLine( _
                        "{0}: {1}", _
                        value.Timestamp, _
                        value)
            Next
        Loop While historyNavigator.MoveNextPage()
    End Using

Different types of aggregation can be chosen for processed reading of the historical data via the OpcAggregateType:

  • For reading the lowest value within a time frame:
    Dim minSpeed = client.ReadNodeHistoryProcessed( _
            startTime, _
            endTime, _
            OpcAggregateType.Minimum, _
            "ns=2;s=Machine/Job/Speed")
  • For reading the average value within a time frame:
    Dim avgSpeed = client.ReadNodeHistoryProcessed( _
            startTime, _
            endTime, _
            OpcAggregateType.Average, _
            "ns=2;s=Machine/Job/Speed")
  • For reading the highest value within a time frame:
    Dim maxSpeed = client.ReadNodeHistoryProcessed( _
            startTime, _
            endTime, _
            OpcAggregateType.Maximum, _
            "ns=2;s=Machine/Job/Speed")

Method Nodes

The following types are used here: OpcClient, OpcNodeId and OpcCallMethod.

The OpcNodeId of the Node determines which method Node is to be called. The hereby expected parameters of a method can be provided via the parameters when calling CallMethod or by the according OpcCallMethod instance. Note that first the OpcNodeId of the owner of the method has to be given and then the OpcNodeId of the method itself. The OpcNodeId of the owner determines the identifier of the object Node or the object type Node, that references the method as a HasComponent reference.

  • Call a single method Node without parameters (the method does not define any IN arguments):
    'The result array contains the values of the OUT arguments offered by the method.
    Dim result As Object() = client.CallMethod( _
            "ns=2;s=Machine", _                     'NodeId of Owner Node
            "ns=2;s=Machine/StartMachine")          'NodeId of Method Node
  • Call a single method Node with parameters (the method defines some IN arguments):
    'The result array contains the values of the OUT arguments offered by the method.
    Dim result As Object() = client.CallMethod( _
            "ns=2;s=Machine", _                     'NodeId of Owner Node
            "ns=2;s=Machine/StopMachine", _         'NodeId of Method Node
            "Job Change", _                         'Parameter 1: 'reason'
            10023, _                                'Parameter 2: 'reasonCode'
            DateTime.Now)                           'Parameter 3: 'scheduleDate'
  • Call several method Nodes without parameters (the methods do not define any IN arguments):
    Dim commands As OpcCallMethod() = New OpcCallMethod() { _
        New OpcCallMethod("ns=2;s=Machine", "ns=2;s=Machine/StopMachine"), _
        New OpcCallMethod("ns=2;s=Machine", "ns=2;s=Machine/ScheduleJob"), _
        New OpcCallMethod("ns=2;s=Machine", "ns=2;s=Machine/StartMachine") _
    }
     
    'The result array contains the values of the OUT arguments offered by the methods.
    Dim results As Object()() = client.CallMethods(commands)
  • Call several method Nodes with parameters (the methods do define some IN arguments):
    Dim commands As OpcCallMethod() = New OpcCallMethod() { _
        New OpcCallMethod( _
                "ns=2;s=Machine", _                 'NodeId of Owner Node
                "ns=2;s=Machine/StopMachine", _     'NodeId of Method Node
                "Job Change", _                     'Parameter 1: 'reason'
                10023, _                            'Parameter 2: 'reasonCode'
                DateTime.Now), _                    'Parameter 3: 'scheduleDate'
        New OpcCallMethod( _
                "ns=2;s=Machine", _                 'NodeId of Owner Node
                "ns=2;s=Machine/ScheduleJob", _     'NodeId of Method Node
                "MAN_F01_78910"), _                 'Parameter 1: 'jobSerial'
        New OpcCallMethod( _
                "ns=2;s=Machine", _                 'NodeId of Owner Node
                "ns=2;s=Machine/StartMachine", _    'NodeId of Method Node
                10021) _                            'Parameter 1: 'reasonCode'
    }
     
    'The result array contains the values of the OUT arguments offered by the methods.
    Dim results As Object()() = client.CallMethods(commands)

File Nodes

Nodes of the type FileType define per definition of the OPC UA specification certain properties (= Property Nodes) and methods (= Method Nodes) that allow access to a data stream as if your were accessing a file in the file system. Hereby information about the content of the logical and physical file is provide exclusively. According to the specification, a possibly existing path to the file is not provided. The access to the file itself is realized via Open, Close, Read, Write, GetPosition and SetPosition. The data is always processed in a binary way. Like on every other platform in the OPC UA you can choose a mode while opening Open which sets the kind of data access planned. You can also request exclusive access to a file in the OPC UA. After calling the Open method you recieve a numeric key for further file handle. This key always has to be handed over at the methods Read, Write, GetPosition and SetPosition. An open file has to be closed again when no longer needed.

Access to Nodes of the type FileType can be executed manually via the OpcClient by using the ReadNode and CallMethod functions. As an alternative the Framework provides numerous other classes that - modelled after the .NET Framework - allow access to Nodes of the type FileType. The OpcNodeId of the Node determines which “File Node” shall be accessed.

Data access with the OpcFile class:

  • Reading the entire content of a text file:
    Dim reportText As String = OpcFile.ReadAllText(client, "ns=2;s=Machine/Report")
  • Appending further text data to a text file:
    OpcFile.AppendAllText(client, "ns=2;s=Machine/Report", "Lorem ipsum")
  • Opening and reading the file via OpcFileStream:
    Using stream = OpcFile.OpenRead(client, "ns=2;s=Machine/Report")
        Dim reader = New StreamReader(stream)
     
        While Not reader.EndOfStream
            Console.WriteLine(reader.ReadLine())
        End While
    End Using
  • Opening and writing the file via OpcFileStream:
    Using stream = OpcFile.OpenWrite(client, "ns=2;s=Machine/Report")
        Dim writer = New StreamWriter(stream)
     
        writer.WriteLine("Lorem ipsum")
        writer.WriteLine("dolor sit")
        '...
    End Using

Data access with the OpcFileInfo class:

  • Creating an OpcFileInfo instance:
    Dim file = New OpcFileInfo(client, "ns=2;s=Machine/Report")
  • Working with the OpcFileInfo instance:
    If file.Exists Then
        Console.WriteLine("File Length: {0}", file.Lengh)
     
        If file.CanUserWrite Then
            Using stream = file.OpenWrite()
                'Your code to write via stream.
            End Using
        Else
            Using stream = file.OpenRead()
                'Your code to read via stream.
            End Using
        End If
    End If

Data access with the OpcFileMethods class:

  • via .NET SafeHandle concept (realized via the SafeOpcFileHandle class):
    Using handle = OpcFileMethods.SecureOpen(client, "ns=2;s=Machine/Report", OpcFileMode.ReadWrite)
        Dim data As Byte() = OpcFileMethods.SecureRead(handle, 100)
     
        Dim position As Long = OpcFileMethods.SecureGetPosition(handle)
        OpcFileMethods.SecureSetPosition(handle, position + data(data.Length - 1))
     
        OpcFileMethods.SecureWrite(handle, New Byte() {1, 2, 3})
    End Using
  • via numeric File Handle:
    Dim handle As UInteger = OpcFileMethods.Open(client, "ns=2;s=Machine/Report", OpcFileMode.ReadWrite)
     
    Try
        Dim data As Byte() = OpcFileMethods.Read(client, "ns=2;s=Machine/Report", handle, 100)
     
        Dim position As ULong = OpcFileMethods.GetPosition(client, "ns=2;s=Machine/Report", handle)
        OpcFileMethods.SetPosition(client, "ns=2;s=Machine/Report", handle, position + data(data.Length - 1))
     
        OpcFileMethods.Write(client, "ns=2;s=Machine/Report", handle, New Byte() {1, 2, 3})
    Finally
        OpcFileMethods.Close(client, "ns=2;s=Machine/Report", handle)
    End Try

Only file accesses via OpcFile, OpcFileInfo, OpcFileStream and SafeOpcFileHandle guarantee an also implicit release of an open file, even if the call of the Close method has been “forgotten”. When closing the connection to the Server, at the latest, all open files are closed by the OpcClient automatically. However, this is not the case, if the methods of the class OpcFileMethods are used without the “Secure” prefix.

The OPC UA specification does not define a way to determine a Node as a Node of the type FileType and therefore as a File-Node. For this the Framework offers the option to identify a File-Node via its Node structure:

If OpcFileMethods.IsFileNode(client, "ns=2;s=Machine/Report") Then
    'Your code to operate on the file node.
End If

Datatype Nodes

The following types are used here: OpcClient, OpcNodeId, OpcNodeInfo and OpcTypeNodeInfo.

In case there a Server provides a Node, through that the Server publishes information about a Server defined Datatype, it can be necessary that a Client wants to query such type information. The simplest way to do so is Browsing. The framework provides especially for Datatype Nodes a specialisation of the OpcNodeInfo - the OpcTypeNodeInfo. Using the properties of this class additional information can be queried about the user defined Datatype. For example whether the Datatype is an enumeration. In this case it is possible to query the different enum-entries as well. This works as follows:

Dim machineStatusNode = TryCast(client.BrowseNode("ns=2;s=MachineStatus"), OpcTypeNodeInfo)
 
If machineStatusNode IsNot Nothing AndAlso machineStatusNode.IsEnum Then
    Dim members = machineStatusNode.GetEnumMembers()
 
    For Each member In members
        Console.WriteLine(member.Name)
    Next
End If

Data Nodes

The following types are used here: OpcClient, OpcNodeId, OpcNodeInfo and OpcVariableNodeInfo.

Working with Data Nodes is primarily restricted to Reads and Writes regarding the Value-Attribut of the Node. This works like on every other Node as already explained in “Reading Values of Node(s)” and “Writing Values of Node(s)”. In case there additional information is required except of the value of the Node (without the need to manually retrieve them using ReadNode requests), then they can be directly and simple requested from the Server using Browsing. The following sample represents how this does work:

Dim machineStatusNode = TryCast(client.BrowseNode("ns=2;s=Machine/Status"), OpcVariableNodeInfo)
 
If machineStatusNode IsNot Nothing Then
    Console.WriteLine("AccessLevel: {0}", machineStatusNode.AccessLevel)
    Console.WriteLine("UserAccessLevel: {0}", machineStatusNode.UserAccessLevel)
    ...
End If

Furthermore it is possible to continue the Browsing on the Datatype Node used by the Data Node:

Dim dataTypeNode = machineStatusNode.DataType
 
If dataTypeNode.IsSystemType Then
    ...
End If

Data-Item Nodes

The following types are used here: OpcClient und OpcNodeId.

Data-Item Nodes, provided by the OpcDataItemNode through a Server, are an extension of the simple OpcDataVariableNode. They are also primarily used for value provision. Beyond from that they provide additional useful Metadata. One of that additional information is published through the Definition property which is defined as Property-Node which is in turn a child of the Data-Item Node. This property is used to ensure the correct processing and interpreation of the data provided by the Node. The value of the property is vendor specific and shall inform the user how the value is achieved. Apart from that it is also possible to work with the Node as represented in “Reading Values of Node(s)” and “Writing Values of Node(s)”.

Data-Item Nodes for analog Values

The following types are used here: OpcClient, OpcNodeId, OpcNodeInfo and OpcAnalogItemNodeInfo.

This kind of Node represents a specialisation of the Data-Item Node OpcDataItemNode. The values provided are especially used in analog environments and can be described througth the additional properties InstrumentRange, EngineeringUnit and EngineeringUnitRange. While the InstrumentRange describes the value range of the analog data of its source, the EngineeringUnit defines the measure of the provided values. Is the value determined under normal conditions, then it is within the customizable value range defined by the EngineeringUnitRange property. The EngineeringUnit property described measure is defined by the UNECE Recommendation N° 20. These recommendations are based on the International System of Units (short SI Units). The OpcAnalogItemNodeInfo provides an appropriate interface for simplified processing of the provided information of such a Node through Browsing:

Dim temperatureNode = TryCast(client.BrowseNode("ns=2;s=Machine/Temperature"), OpcAnalogItemNodeInfo)
 
If temperatureNode IsNot Nothing Then
    Console.WriteLine("InstrumentRange: {0}", temperatureNode.InstrumentRange)
 
    Console.WriteLine("EngineeringUnit: {0}", temperatureNode.EngineeringUnit)
    Console.WriteLine("EngineeringUnitRange: {0}", temperatureNode.EngineeringUnitRange)
End If

The information contained in an OpcEngineeringUnitInfo describes a unit of measurement of the UNECE table for units of measurement. The possible units can be looked up at the OPC Foundation: UNECE units of measurement in OPC UA (currently not available). Alternatively, the MTConnect Institute also offers the UNECE table on GitHub: UNECE Units of Measure in OPC UA (MTConnect@GitHub).

Event Subscription

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

Events inform a subscriber (such as Clients) about operations, conditions, and system-specific events. Such information can be delivered to interested parties directly via global events. A global event is always published through the Server's “Server” Node. For this reason, global events are also subscribed via the Server Node as follows:

client.SubscribeEvent(OpcObjectTypes.Server, AddressOf HandleGlobalEvents)

An event is generally handled with a method of the following signature:

Private Shared Sub HandleGlobalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Console.WriteLine(e.Event.Message)
End Sub

The event data received via the OpcEventReceivedEventArgs instance can be retrieved via the Event property. The property always returns an instance of the type OpcEvent. If the received event data is a specialization of the OpcEvent class, it can be simply cast as follows:

Private Shared Sub HandleGlobalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Dim alarm = TryCast(e.Event, OpcAlarmCondition)
 
    If alarm IsNot Nothing Then
        Console.WriteLine("Alarm: " & alarm.Message)
    End If
End Sub

The call shown above subscribes to all events published globally by the Server. In order to restrict the information received as well as the events which are ever sent from the Server to the Client, an event filter can be defined and transmitted to the Server when a subscription is concluded:

'Define an attribute operand using the identifier of the type which defines the
'attribute / property including the name of the attribute / property to evaluate
'by the operand.
Dim severity = New OpcSimpleAttributeOperand(OpcEventTypes.Event, "Severity")
Dim conditionName = New OpcSimpleAttributeOperand(OpcEventTypes.Condition, "ConditionName")
 
Dim filter = OpcFilter.Using(client) _
        .FromEvents(OpcEventTypes.AlarmCondition) _
        .Where(severity > OpcEventSeverity.Medium And conditionName.Like("Temperature")) _
        .Select()
 
client.SubscribeEvent(
        OpcObjectTypes.Server,
        filter,
        AddressOf HandleGlobalEvents)

Creating an event filter always requires a OpcClient instance, which must already have a connection to the destination Server. This is required because the Client collects the type information from the Server relevant for the assembling of the filter when assembling. In the above example, all properties of the Node type OpcAlarmConditionNode are recursively collected up to the OpcNode via the Client, and according to the Where and Select clauses rules for the selection of the Event data are created. The Node types to be analyzed can be passed with comma separated to the FromEvents(…)- method:

Dim severity = New OpcSimpleAttributeOperand(OpcEventTypes.Event, "Severity")
Dim conditionName = New OpcSimpleAttributeOperand(OpcEventTypes.Condition, "ConditionName")
 
Dim filter = OpcFilter.Using(client) _
        .FromEvents( _
            OpcEventTypes.AlarmCondition, _
            OpcEventTypes.ExclusiveLimitAlarm, _
            OpcEventTypes.DialogCondition) _
        .Where(severity > OpcEventSeverity.Medium And conditionName.Like("Temperature")) _
        .Select()
 
client.SubscribeEvent(
        OpcObjectTypes.Server,
        filter,
        AddressOf HandleGlobalEvents)

The Where(…) method can then be used to restrict the information collected by FromEvents(…) about the properties provided by the Node types. For this, the framework offers various operator overloads (<=, <, >, >= and ==). For logical combinations of the operands it also offers the logical operators OR (|) and AND (&). In addition, various methods are available for further restrictions, such as: Like, Between, InList, IsNull, Not and OfType.

After the restriction has been made, you can use the Select(…) method to select the properties that will be additionally selected by the Server when the event occurs (which corresponds to the conditions specified under Where(…)) and are transferred to the Client.

Event Nodes

The following types are used: OpcClient, OpcEventReceivedEventArgs, OpcEvent und OpcSubscription.

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

It is not always appropriate for a Server to send events globally through the Server Node to all subscribers. Often the context therefore plays a crucial role in whether an event is of interest to a subscriber. Event Nodes are used to define local events. If the Server uses an Event Node to provide event data for a Node, then the Event Node is a so-called “Notifier” of the Node. For this reason, it is also possible to recognize by a HasNotifier reference which event Node reports event data to a Node. It should be noted that an Event Node may be a “Notifier” for multiple Nodes. As a result, local events are always subscribed via the notified Nodes:

client.SubscribeEvent(machineNodeId, AddressOf HandleLocalEvents)

An event is generally handled with a method of the following signature. The further processing of the event data is identical to the processing of global events (see section 'Working with Events').

Private Shared Sub HandleLocalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Console.WriteLine(e.Event.Message)
End Sub

Of course, local events, as shown in the section 'Working with Events', can also be filtered.

Generally, after a subscriber (a Client) is only informed of events as long as he is in contact with the Server and has initiated a subscription, a subscriber will not know what events have already occurred prior to establishing a connection to the Server. If the Server is to inform subscribers of past events, subscribers can request them from the Server as follows. In general, however, a Server is not obliged to provide past events.

Dim subscription = client.SubscribeEvent( _
        machineNodeId, _
        AddressOf HandleLocalEvents)
 
'Query most recent event information.
subscription.RefreshConditions()

Event Nodes with Conditions

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

A specialization of the OpcEvent (featured in the 'Working with Events' section) is the OpcCondition class. It serves to provide event data to which specific conditions are attached. Only in the case that a condition awarded to an Event Node is true will such an event be triggered. The information of the event includes information about the state of the condition as well as information related to the evaluation of the condition. Since this information can vary in complexity depending on the scenario, the OpcCondition represents the base class of all event data to which a condition is attached.

In addition to the general properties of an event (provided by the OpcEvent base class), one instance of the OpcCondition provides information that may be of interest to the Client for further processing of the event. This allows the Client to verify that, in general, the Server evaluates the event as relevant to a Client (see IsRetained property). Similarly, the Client can evaluate the state of the condition, that is, whether it is active or inactive (see IsEnabled property).

Private Shared Sub HandleLocalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Dim condition = TryCast(e.Event, OpcCondition)
 
    If condition.IsRetained Then
        Console.Write((If(condition.ClientUserId, "Comment")) & ":")
        Console.WriteLine(condition.Comment)
    End If
End Sub

If the client does not want to further evaluate the condition, the client can deactivate it:

Private Shared Sub HandleLocalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Dim condition = TryCast(e.Event, OpcCondition)
 
    If condition.IsEnabled AndAlso condition.Severity < OpcEventSeverity.Medium Then
        condition.Disable(client)
    End If
End Sub

In addition, the client can also add a comment to the current state of the condition:

Private Shared Sub HandleLocalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Dim condition = TryCast(e.Event, OpcCondition)
 
    If condition IsNot Nothing Then
        condition.AddComment(client, "Evaluated by me!")
    End If
End Sub

Event Nodes with Dialog Conditions

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

A specialization of the OpcCondition is the OpcDialogCondition. The condition associated with this event is a dialog with the subscribers. In this case, such a condition consists of a prompt, response options and information as to which option should be selected by default (DefaultResponse property), which option to confirm the dialog (OkResponse property) and which is used to cancel the dialog (CancelResponse property). When such a dialog-driven event is triggered, the Server waits for one of the subscribers to provide it with an answer in the form of the choice made based on the given answer options. The condition for further processing, the operations linked to the dialog, is thus the answer to a task, a question, an information or a warning. The information provided for this by the event can be processed as follows and reported back to the Server:

Private Shared Sub HandleLocalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Dim condition = TryCast(e.Event, OpcDialogCondition)
 
    If condition IsNot Nothing AndAlso condition.IsActive Then
        Console.WriteLine(condition.Prompt)
        Console.WriteLine("    Options:")
 
        Dim responseOptions = condition.ResponseOptions
 
        For index As Integer = 0 To responseOptions.Length - 1
            Console.Write("      [{0}] = {1}", index, responseOptions(index).Value)
 
            If index = condition.DefaultResponse Then _
                Console.Write(" (default)")
 
            Console.WriteLine()
        Next
 
        Dim respond = String.Empty
        Dim respondOption = condition.DefaultResponse
 
        Do
            Console.Write("Enter the number of the option and press Enter to respond: ")
            respond = Console.ReadLine()
 
            If String.IsNullOrEmpty(respond) _
                Then Exit Do
        Loop While Not Integer.TryParse(respond, respondOption)
 
        condition.Respond(client, respondOption)
    End If
End Sub

Apart from the standard event handling method, the OpcClient class also provides the DialogRequested event. If OpcDialogCondition event data is received by the Client and not answered by any handler, the Client executes the DialogRequested event to perform a dialog processing on it. This also simplifies the handling of the event data somewhat, since these can be passed on typed to the event handler:

AddHandler(client.DialogRequested, AddressOf HandleDialogRequested);
...
 
Private Shared Sub HandleDialogRequested( _
        ByVal sender As Object, _
        ByVal e As OpcDialogRequestedEventArgs)
    'Just use the default response, here.
    e.SelectedResponse = e.Dialog.DefaultResponse
End Sub

You just need to set the SelectedResponse property of the event arguments. Calling the Respond(…) method of the OpcDialogCondition is done by the OpcClient after the execution of the event handler.

Event Nodes with Feedback Conditions

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

Based on OpcCondition events, the OpcAcknowledgeableCondition is a specialization used as the base class for conditions with feedback requirements. Events of this type define that, when their condition is met, a “report with acknowledgment of receipt” is issued. The “return receipt” - that is the feedback - can be used to control further processes as well as to easily acknowledge hints and warnings. The feedback mechanism provided for this purpose is divided into two stages. While the first stage is a kind of “read receipt”, the second level is a kind of “read receipt with a nod”. OPC UA defines the read receipt as a simple confirmation and the read receipt as a nod with acknowledgment. For both types of recognition, the events provide corresponding Confirm and Acknowledge methods. By definition, the execution of the “acknowledge” process should make an explicit execution of the “confirm” process unnecessary. On the other hand, it is possible to first send a confirmation and then, separately, an acknowledgment. Regardless of the order and the type of feedback, a comment from the operator can optionally be specified for the confirm or acknowledge. An acknowledgment as feedback could be implemented as follows:

Private Shared Sub HandleLocalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Dim condition = TryCast(e.Event, OpcAcknowledgeableCondition)
 
    If condition IsNot Nothing AndAlso Not condition.IsAcked Then
        Console.WriteLine("Acknowledgment is required for condtion: {0}", condition.ConditionName)
        Console.WriteLine("  -> {0}", condition.Message)
        Console.Write("Enter your acknowlegment comment and press Enter to acknowledge: ")
 
        Dim comment = Console.ReadLine()
        condition.Acknowledge(client, comment)
    End If
End Sub

In addition, when processing this type of event, you can check whether the event has already been confirmed by Confirm (see IsConfirmed property) or by Acknowledge (see IsAcked property). It should be noted that a Server must always define the interpretation itsefl as well as the logic following the respective feedback. So whether a Server makes use of both feedback options or only one is left to the respective developer. In the best case, a Server uses at least the Acknowledge method, as it is defined by the specification as “stronger”.

Event Nodes with Alarm Conditions

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

The most important implementation of the OpcAcknowledgeableCondition in OPC UA is the OpcAlarmCondition. With the help of OpcAlarmCondition events it is possible to define events whose behavior is comparable to a bedside timer. Accordingly, such an event becomes active (see IsActive property) if the condition associated with it is met. In the case of an alarm clock, for example, “reaching the alarm time”. For example, an alarm that is set with a wake-up time but should not be activated when it is reached is called a suppressed alarm (see IsSuppressed and IsSuppressedOrShelved property). But if an alarm becomes active, it can be shelved (see IsSuppressedOrShelved property). An alarm can be reset once (“One Shot Shelving”) or in time (“Timed Shelving”) (see Shelving Child Node). Alternatively, a reset alarm can also be “unshelved” again (see Shelving Child Node).

Private Shared Sub HandleLocalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Dim alarm = TryCast(e.Event, OpcAlarmCondition)
 
    If alarm IsNot Nothing Then
        Console.Write("Alarm {0} is", alarm.ConditionName)
        Console.WriteLine("{0}!", If(alarm.IsActive, "active", "inactive"))
    End If
End Sub

Event Nodes with discrete Alarm Conditions

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

Based on the OpcAlarmCondition event data, there are several specializations that have been explicitly defined for specific types of alarms to further specify the form, reason or content of an alarm by the nature of the alarm. A subclass of such self-describing alarms are the discrete alarms. The basis for a discrete alarm is the OpcDiscreteAlarm class. It defines an alarm state that is used to classify types into alarm states, where the input for the alarm can only accept a certain number of possible values (e.g. true / false, running / paused / terminated). If an alarm represents a discrete condition that is considered abnormal, the OpcOffNormalAlarm or a subclass of it will be used. Starting from this alarm class, the framework offers a further concretization with the OpcTripAlarm. The OpcTripAlarm becomes active when, for example, an abnormal fault occurs on a monitored device, e.g. when the motor is shut down due to overload.

Private Shared Sub HandleLocalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Dim alarm = TryCast(e.Event, OpcDiscreteAlarm)
 
    If alarm IsNot Nothing Then
        If TypeOf alarm Is OpcTripAlarm Then
            Console.WriteLine("Trip Alarm!")
        ElseIf TypeOf alarm Is OpcOffNormalAlarm Then
            Console.WriteLine("Off Normal Alarm!")
        End If
    End If
End Sub

Event Nodes with Alarm Conditions for Limits

The following types are used: OpcClient, OpcEventReceivedEventArgs und OpcLimitAlarm.

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

If the Server checks process-specific limit values and then publishes the output of the check for limit value overruns / underruns, then the OpcLimitAlarm class represents the central starting point for entering the classes of limit alarms. Using this class limits are divided into up to four levels. To differentiate them, they are called LowLow, Low, High and HighHigh (called in order of their metric order). By definition, the Server does not need to define all limits.

Private Shared Sub HandleLocalEvents( _
        ByVal sender As Object, _
        ByVal e As OpcEventReceivedEventArgs)
    Dim alarm = TryCast(e.Event, OpcLimitAlarm)
 
    If alarm IsNot Nothing Then
        Console.Write(alarm.LowLowLimit)
        Console.Write(" ≤ ")
        Console.Write(alarm.LowLimit)
        Console.Write(" ≤ ")
        Console.Write(alarm.HighLimit)
        Console.Write(" ≤ ")
        Console.Write(alarm.HighHighLimit)
    End If
End Sub

Event Nodes with Alarm Conditions for exclusive Limits

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

A subclass of the OpcLimitAlarm events is the class OpcExclusiveLimitAlarm. As its name suggests, it serves to define limit alerts for exclusive boundaries. Such a limit alarm uses values for the boundaries that are mutually exclusive. This means that if a limit value has been exceeded / undershot, it is not possible for another limit value to be exceeded or undershot at the same time.

There are three further specializations of the OpcExclusiveLimitAlarm within the OPC UA.

OpcExclusiveDeviationAlarm

This type of alarm is used when a slight deviation from defined limits is detected.

OpcExclusiveLevelAlarm

This type of alarm is used when a limit is exceeded. This typically affects an instrument - such as a temperature sensor. This type of alarm becomes active when the observed value is above an upper limit or below a lower limit.

OpcExclusiveRateOfChangeAlarm

This type of alarm is used to report an unusual change or missing change in a measured value with respect to the rate at which the value has changed. The alarm becomes active if the rate at which the value changes exceeds or falls below a defined limit.

Event Nodes with Alarm Conditions for non-exclusive Limits

This section describes a part of the API for topics related to: Alarm & Events, Alarm & Conditions.

A subclass of the OpcLimitAlarm events is the class OpcNonExclusiveLimitAlarm. As its name suggests, it serves to define limit alerts for non-exclusive boundaries. Such a limit alarm uses values for the limits that are not mutually exclusive. This means that when a limit has been exceeded / undershot, that at the same time another limit may be exceeded / undershot. The limits that are thereby violated can be checked with the properties IsLowLow, IsLow, IsHigh and IsHighHigh of the event data.

There are three further specializations of the OpcNonExclusiveLimitAlarm within the OPC UA.

OpcNonExclusiveDeviationAlarm

This type of alarm is used when a slight deviation from defined limits is detected.

OpcNonExclusiveLevelAlarm

This type of alarm is used when a limit is exceeded. This typically affects an instrument - such as a temperature sensor. This type of alarm becomes active when the observed value is above an upper limit or below a lower limit.

OpcNonExclusiveRateOfChangeAlarm

This type of alarm is used to report an unusual change or missing change in a measured value with respect to the rate at which the value has changed. The alarm becomes active if the rate at which the value changes exceeds or falls below a defined limit.

Adding Nodes

While a Server provides a predefined set of “default nodes” to its Clients, the Clients can cause the Server to provide additional nodes. This is done by using the “AddNodes” interface of the server. Starting from the OpcAddNode class, the framework provides many further subclasses that can be used to create type-specific nodes. A new folder node can thus be created in the following way:

Dim result As OpcAddNodeResult = client.AddNode(New OpcAddFolderNode( _
        name:="Jobs", _
        nodeId:=OpcNodeId.Null, _
        parentNodeId:="ns=2;s=Machine"))

The parameters used provide the necessary minimum of information required. The first parameter “name” is used for the Name property, the DisplayName property, the SymbolicName property, and the Description property of the Node. The second parameter “nodeId” tells the Server which identifier is to be used for the node. In case there the identifier is already used in the address space of the Nodes of the Server for another Node, the Node is not created and the Client receives the result code “BadNodeIdRejected”. If OpcNodeId.Null is used instead, as shown in the example, the server will automatically create and assign a new identifier for the Node. The parameter “parentNodeId” finally defines the identifier of the parent Node under which the new Node is to be created in the tree.

Calling the AddNode method returns an instance of the OpcAddNodeResult class. In addition to the information about the result of the operation, it also provides the identifier that was finally used for the new Node:

If result.IsGood
    Console.WriteLine("NodeId of 'Jobs': {0}", result.NodeId)
Else
    Console.WriteLine($"Failed to add node: {0}", result.Description)
End If

Besides the possibility to add a single Node, several Nodes can be added simultaneously:

Dim jobsNodeId As OpcNodeId = result.NodeId
 
Dim results As OpcAddNodeResultCollection = client.AddNodes( _
        New OpcAddDataVariableNode(Of String)("CurrentJob", jobsNodeId), _
        New OpcAddDataVariableNode(Of String)("NextJob", jobsNodeId), _
        New OpcAddDataVariableNode(Of Integer)("NumberOfJobs", jobsNodeId))

Calling the AddNodes method returns an instance of the OpcAddNodeResultCollection class. Which contains OpcAddNodeResult instances that can be evaluated and processed in the same way as described above.

In addition to the possibility of adding one or more Nodes at the same time, it is possible to pass entire trees of Nodes to the methods AddNode and AddNodes:

Dim results As OpcAddNodeResultCollection = client.AddNodes( _
        New OpcAddObjectNode( _
                "JOB001", _
                nodeId:=OpcNodeId.Null, _
                parentNodeId:=jobsNodeId, _
                New OpcAddDataVariableNode(Of SByte)("Status", -1), _
                New OpcAddDataVariableNode(Of String)("Serial", "J01-DX-11.001"), _
                New OpcAddAnalogItemNode(Of Single)("Speed", 1200F) With {
                    .EngineeringUnit = New OpcEngineeringUnitInfo(5067859, "m/s", "metre per second"),
                    .EngineeringUnitRange = New OpcValueRange(5400, 0),
                    .Definition = "DB100.DBW 0"
                }, _
                New OpcAddObjectNode( _
                        "Setup", _
                        New OpcAddPropertyNode(Of Boolean)("UseCutter"), _
                        New OpcAddPropertyNode(Of Boolean)("UseDrill")), _
                New OpcAddObjectNode( _
                        "Schedule", _
                        New OpcAddPropertyNode(Of DateTime)("EarliestStartTime"), _
                        New OpcAddPropertyNode(Of DateTime)("LatestStartTime"), _
                        New OpcAddPropertyNode(Of TimeSpan)("EstimatedRunTime"))), _
        New OpcAddObjectNode( _
                "JOB002", _
                nodeId:=OpcNodeId.Null, _
                parentNodeId:=jobsNodeId, _
                New OpcAddDataVariableNode(Of SByte)("Status", -1), _
                New OpcAddDataVariableNode(Of String)("Serial", "J01-DX-53.002"), _
                New OpcAddAnalogItemNode(Of Single)("Speed", 3210F) With {
                    .EngineeringUnit = New OpcEngineeringUnitInfo(5067859, "m/s", "metre per second"),
                    .EngineeringUnitRange = New OpcValueRange(5400, 0),
                    .Definition = "DB200.DBW 0" _
                }, _
                New OpcAddObjectNode( _
                        "Setup", _
                        New OpcAddPropertyNode(Of Boolean)("UseCutter"), _
                        New OpcAddPropertyNode(Of Boolean)("UseDrill")), _
                New OpcAddObjectNode( _
                        "Schedule", _
                        New OpcAddPropertyNode(Of DateTime)("EarliestStartTime"), _
                        New OpcAddPropertyNode(Of DateTime)("LatestStartTime"), _
                        New OpcAddPropertyNode(Of TimeSpan)("EstimatedRunTime"))))

Such a tree can also be constructed using the corresponding properties:

Dim jobsNodeId = result.NodeId
 
Dim job = New OpcAddObjectNode( _
        name:="JOB003", _
        nodeId:=OpcNodeId.Null, _
        parentNodeId:=jobsNodeId)
 
job.Children.Add(New OpcAddDataVariableNode(Of SByte)("Status", -1))
job.Children.Add(New OpcAddDataVariableNode(Of String)("Serial", "J01-DX-78.003"))
job.Children.Add(new OpcAddAnalogItemNode(Of Single)("Speed", 1200f) With {
    .EngineeringUnit = new OpcEngineeringUnitInfo(5067859, "m/s", "metre per second"),
    .EngineeringUnitRange = new OpcValueRange(5400, 0),
    .Definition = "DB100.DBW 0" _
})
 
Dim setup = New OpcAddObjectNode("Setup")
setup.Children.Add(New OpcAddPropertyNode(Of Boolean)("UseCutter"))
setup.Children.Add(New OpcAddPropertyNode(Of Boolean)("UseDrill"))
 
job.Children.Add(setup)
 
Dim schedule = New OpcAddObjectNode("Schedule")
schedule.Children.Add(New OpcAddPropertyNode(Of DateTime)("EarliestStartTime"))
schedule.Children.Add(New OpcAddPropertyNode(Of DateTime)("LatestStartTime"))
schedule.Children.Add(New OpcAddPropertyNode(Of TimeSpan)("EstimatedRunTime"))
 
job.Children.Add(schedule)
 
Dim result As OpcAddNodeResult = client.AddNode(job)

In case there other type definitions shall be used for the Nodes than those provided by the corresponding subclasses of OpcAddNode, then it is possible to create object and variable Nodes based on their type definition:

client.AddObjectNode(OpcObjectType.DeviceFailureEventType, "FailureInfo")
client.AddVariableNode(OpcVariableType.XYArrayItem, "Coordinates")

Unlike adding nodes based on Foundation-defined type definitions, it is also possible to add nodes based on the identifier of their type definition. For this, the type to be used must be declared in advance via an object or variable-specific type definition. This can then be used again and again to add corresponding nodes:

'Declare Job Type
Dim jobType = OpcAddObjectNode.OfType(OpcNodeId.Of("ns=2;s=Types/JobType"))
 
client.AddNodes( _
        jobType.Create("JOB001", nodeId:=OpcNodeId.Null, parentNodeId:=jobsNodeId), _
        jobType.Create("JOB002", nodeId:=OpcNodeId.Null, parentNodeId:=jobsNodeId), _
        jobType.Create("JOB003", nodeId:=OpcNodeId.Null, parentNodeId:=jobsNodeId), _
        jobType.Create("JOB004", nodeId:=OpcNodeId.Null, parentNodeId:=jobsNodeId))
 
Dim scheduleNodeId = OpcNodeId.Parse("ns=2;s=Machine/JOB002/Schedule")
 
'Declare Shift Time Type
Dim shiftTimeType = OpcAddVariableNode.OfType(OpcNodeId.Of("ns=2;s=Types/ShiftTimeType"))
 
Dim result As OpcAddNodeResult = client.AddNode(New OpcAddObjectNode( _
        "ShiftPlanning", _
        nodeId:=OpcNodeId.Null, _
        parentNodeId:=scheduleNodeId, _
        shiftTimeType.Create("Early"), _
        shiftTimeType.Create("Noon"), _
        shiftTimeType.Create("Late")))

Deleting Nodes

The following types are used: OpcClient, OpcDeleteNode, OpcStatus and OpcStatusCollection.

Server-provided and client-added nodes can be deleted using the Server's “DeleteNodes” interface. This requires primarily the identifier of the node to be deleted:

Dim result As OpcStatus = client.DeleteNode("ns=2;s=Machine/Jobs")

The possibility shown in the above example uses an instance of the OpcDeleteNode class for deletion, which by default also includes the deletion of all references pointing to the Node. However, if the references to the Node are to be retained, the parameter “includeTargetReferences” have to be set to the value “false”:

Dim result As OpcStatus = client.DeleteNode( _
        "ns=2;s=Machine/Jobs", _
        includeTargetReferences:=False)

Besides the possibility to delete a single Node, several Nodes can be deleted at the same time:

Dim results As OpcStatusCollection = client.DeleteNodes( _
        New OpcDeleteNode("ns=2;s=Machine/Jobs/JOB001"), _
        New OpcDeleteNode("ns=2;s=Machine/Jobs/JOB002"), _
        New OpcDeleteNode("ns=2;s=Machine/Jobs/JOB003"))

Adding References

The following types are used: OpcClient, OpcAddReference, OpcStatus and OpcStatusCollection.

Nodes that already exist in the address space of a Server can have different relationships to one another. These relationships are described by so-called references in the address space of the Server. While nodes are physically placed in the role of the parent or child Node, there may be more logical relationships between them. These relationships thus serve to define more precisely the function and dependencies of the Nodes with each other. In addition, such trees can be used to define additional trees in the address space of the Server without having to reorganize existing nodes.

To add a reference, the Client uses the Server's “AddReferences” interface as follows:

client.AddReference( _
        "ns=2;s=Machines/MAC01", _
        targetNodeId:="ns=2;s=Plant", _
        targetNodeCategory:=OpcNodeCategory.Object)

The example shown adds a relationship starting from the Node with the identifier “ns=2;s=Plant” to the Node “ns=2;s=Machines/MAC01” of the type “Organizes”.

The statement shown corresponds 1:1 to the result of the following example:

client.AddReference( _
        "ns=2;s=Machines/MAC01", _
        targetNodeId:="ns=2;s=Plant", _
        targetNodeCategory:=OpcNodeCategory.Object, _
        direction:=OpcReferenceDirection.ParentToChild, _
        referenceType:=OpcReferenceType.Organizes)

While the first identifier always indicates the source Node, the parameter “targetNodeId” specifies the identifier of the destination Node. The additional parameter “targetNodeCategory” must correspond to the Category property (= NodeClass according to Foundation) of the destination Node, because this ensures that the Server knows that the desired destination Node is sufficiently known.

In addition to the possibility to add a single reference, several references can be added simultaneously:

client.AddReferences( _
        New OpcAddReference("ns=2;s=Machines/MAC01", "ns=2;s=Plant01", OpcNodeCategory.Object), _
        New OpcAddReference("ns=2;s=Machines/MAC02", "ns=2;s=Plant01", OpcNodeCategory.Object), _
        New OpcAddReference("ns=2;s=Machines/MAC03", "ns=2;s=Plant02", OpcNodeCategory.Object))

The example above organizes three nodes each representing one machine for itself below the “Machines” Node below the “Plant01” and the “Plant02” Nodes. The “Organizes” relationship used here by default means that the nodes are still available below the “Machines” Node, but also below the “Plant01” Node, the Nodes “MAC01” and “MAC02”, as well as the Node “MAC03” below the Node “Plant02”.

Deleting References

The following types are used: OpcClient, OpcDeleteReference, OpcStatus and OpcStatusCollection.

Already existing references between the Nodes in the address space of the Server can be deleted via the “DeleteReferences” interface of the Server:

client.DeleteReference( _
        nodeId:="ns=2;s=Machines/MAC03", _
        targetNodeId:="ns=2;s=Plant")

In this case, the example shown deletes all “Organizes” references in the direction of the Node with the identifier “ns=2;s=Machines/MAC03” as well as in the direction of the Node with the identifier “ns=2;s=Plant” between the both Nodes exist.

If, on the other hand, you want to delete only “Organizes” references from the source to the destination Node in one specific direction, you can do this as follows:

client.DeleteReference( _
        nodeId:="ns=2;s=Machines/MAC03", _
        targetNodeId:="ns=2;s=Plant", _
        direction:=OpcReferenceDirection.ChildToParent)

If, on the other hand, you want to delete references that are not of the “Organizes” type, they can be specified using the additional “referenceType” or “referenceTypeId” parameter as follows:

client.DeleteReference( _
        nodeId:="ns=2;s=Machines/MAC03", _
        targetNodeId:="ns=2;s=Plant", _
        direction:=OpcReferenceDirection.ChildToParent, _
        referenceType:=OpcReferenceType.HierarchicalReferences)

In addition to the possibility to delete individual references, several references can be deleted at the same time:

client.DeleteReferences( _
        New OpcDeleteReference("ns=2;s=Machines/MAC01", "ns=2;s=Plant01"), _
        New OpcDeleteReference("ns=2;s=Machines/MAC02", "ns=2;s=Plant01"), _
        New OpcDeleteReference("ns=2;s=Machines/MAC03", "ns=2;s=Plant02"))

General Configuration

The following types are used here: OpcClient, OpcCertificateStores und OpcCertificateStoreInfo.

In all code snippets depicted here the Client is always configured via the Code (if the default configuration of the Client is not applied). The OpcClient instance is the certral port for configuring the Client application, the session parameter and the connection parameter. All settings concerning security can be found as an instance of the OpcClientSecurity class via the Security property of the Client. All settings concerning the Certificate Store can be found as an instance of the OpcCertificateStores class via the CertificateStores property of the Client.

If the Client shall also be configurable via XML, the configuration of the Client can be loaded either from a specific or a random XML file. The necessary steps are described in the section “Preparing the Client Configuration via XML”.

As soon as the according preparations for configuring the Client configuration via XML have been made, the settings can be loaded as follows:

  • Loading the configuration file via the App.config
    client.Configuration = OpcApplicationConfiguration.LoadClientConfig("Opc.UaFx.Client")
  • Loading the configuration file via the path to the XML file
    client.Configuration = OpcApplicationConfiguration.LoadClientConfigFile("MyClientAppNameConfig.xml")

Amongst others, the following options are provided for configuring the Client application:

  • Configuring the application
    • via Code:
      'Default: Value of AssemblyTitleAttribute of entry assembly.
      client.ApplicationName = "MyClientAppName"
       
      'Default: Nothing to auto complete on connect to "urn::" + ApplicationName
      client.ApplicationUri = "http://my.clientapp.uri/"
    • via XML (underneath the OpcApplicationConfiguration element):
        <ApplicationName>MyClient Configured via XML</ApplicationName>
        <ApplicationUri>http://myclient/application</ApplicationUri>
  • Configuring the session parameters
    • via Code:
      client.SessionTimeout = 30000           'Default: 60000
      client.SessionName = "My Session Name"  'Default: Nothing
    • via XML (underneath the OpcApplicationConfiguration element):
          <DefaultSessionTimeout>600000</DefaultSessionTimeout>
  • Configuring the connection parameters
    • via Code:
      client.OperationTimeout = 10000          'Default: 60000
      client.DisconnectTimeout = 5000          'Default: 10000
      client.ReconnectTimeout = 5000           'Default: 10000
    • via XML (underneath the OpcApplicationConfiguration element):
          <OperationTimeout>120000</OperationTimeout>
  • Configuring the Certificate Store
    • via Code:
      'Default: ".\CertificateStores\Trusted"
      client.CertificateStores.ApplicationStore.Path _
              = "%LocalApplicationData%\MyClientAppName\App Certificates"
       
      'Default: ".\CertificateStores\Rejected"
      client.CertificateStores.RejectedStore.Path _
              = "%LocalApplicationData%\MyClientAppName\Rejected Certificates"
       
      'Default: ".\CertificateStores\Trusted"
      client.CertificateStores.TrustedIssuerStore.Path _
              = "%LocalApplicationData%\MyClientAppName\Trusted Issuer Certificates"
       
      'Default: ".\CertificateStores\Trusted"
      client.CertificateStores.TrustedPeerStore.Path _
              = "%LocalApplicationData%\MyClientAppName\Trusted Peer Certificates"
    • via XML (underneath the OpcApplicationConfiguration element):
        <SecurityConfiguration>
          <ApplicationCertificate>
            <StoreType>Directory</StoreType>
            <StorePath>.\CertificateStores\App</StorePath>
            <SubjectName>CN=MyClient, C=US, S=Arizona, O=YourCompany, DC=localhost</SubjectName>
            <!-- <Thumbprint>3a35fb798fc6dee8a7e7e4652b0e28fc14c6ee0f</Thumbprint> -->
          </ApplicationCertificate>
       
          <TrustedIssuerCertificates>
            <StoreType>Directory</StoreType>
            <StorePath>.\CertificateStores\Trusted</StorePath>
          </TrustedIssuerCertificates>
       
          <TrustedPeerCertificates>
            <StoreType>Directory</StoreType>
            <StorePath>.\CertificateStores\Trusted</StorePath>
          </TrustedPeerCertificates>
       
          <NonceLength>32</NonceLength>
       
          <RejectedCertificateStore>
            <StoreType>Directory</StoreType>
            <StorePath>.\CertificateStores\Rejected</StorePath>
          </RejectedCertificateStore>
        </SecurityConfiguration>

Certificate Configuration

Certificates of the type .der, .pem, .pfx and .p12 are recommended. If the Client shall use a secure Server endpoint (where the OpcSecurityMode equals Sign or SignAndEncrypt), the certificate has to have a private key.

  1. An existing certificate is loaded from any path:
    Dim certificate = OpcCertificateManager.LoadCertificate("MyClientCertificate.pfx")
  2. A new certificate is generated (in memory):
    Dim certificate = OpcCertificateManager.CreateCertificate(client)
  3. Save a certificate in any path:
    OpcCertificateManager.SaveCertificate("MyClientCertificate.pfx", certificate)
  4. Set the Client certificate:
    client.Certificate = certificate
  5. The certificate has to be stored in the Application Store:
    If Not client.CertificateStores.ApplicationStore.Contains(certificate)) Then
        client.CertificateStores.ApplicationStore.Add(certificate)
    End If
  6. If no or an invalid certificate is used, a new certificate is generated / used by default. If the Client shall only use the mentioned certificate this function has to be deactivated. For deactivating the function set the property AutoCreateCertificate to the value false:
    client.CertificateStores.AutoCreateCertificate = False

User Identity Configuration

If a Server is expecting additional information about the user identity other than the Client certificate, the user has to be set via the UserIdentity property. You can choose between identities based on username-password and a certificate. If the Server supports an anonymized user identity, no special identity has to be set.

  • Setting a user identity consisting of username-password:
    client.Security.UserIdentity = New OpcClientIdentity("userName", "password")
  • Setting a user identity via certificate (with private key):
    client.Security.UserIdentity = New OpcCertificateIdentity(New X509Certificate2("Doe.pfx"))
  • Setting an anonymous user identity (pre-configured by default):
    client.Security.UserIdentity = Nothing

Server Endpoint Configuration

By default the Client chooses the Server endpoint with the simplest security configuraton. Hereby it chooses an endpoint with the OpcSecurityMode of None, Sign or SignAndEncrypt. According to the OPC Foundation the level of a policy of an endpoint serves as a relative measure for security mechanisms used by the endpoint. Per definition an endpoint with a higher level is more secure than an endpoint with a lower level. By default the Client ignores the Policy-Level of the endpoints.

  1. If the Client shall exclusively consider secure endpoints, the UseOnlySecureEndpoints property has to be set to the value true:
    client.Security.UseOnlySecureEndpoints = True
  2. If the Client shall choose an endpoint defining the highest Policy-Level, the UseHighLevelEndpoint property has to be set to the value true:
    client.Security.UseHighLevelEndpoint = True;
  3. If the Client shall choose an endpoint with the best security configuration, the EndpointPolicy property has to be set as follows:
    client.Security.EndpointPolicy = New OpcSecurityPolicy( _
            OpcSecurityMode.None, OpcSecurityAlgorithm.Basic256)
  4. To examine the endpoints provided by the Server use the OpcDiscoveryClient:
    Using client As New OpcDiscoveryClient("opc.tcp://localhost:4840/")
        Dim endpoints = client.DiscoverEndpoints()
     
        For Each endpoint in endpoints
            'Your code to operate on each endpoint.
        Next
    End Using

Further Security Settings

A Server sends its certificate to the Client for authentication whilst connecting. Using the Server certificate, the Client can decide if to establish a connection to this Server and therefore trust it.

  • For additional checking of the domains deposited in the Server certificate the property VerifyServersCertificateDomains can be used (deactivated by default):
    client.Security.VerifyServersCertificateDomains = True
  • If the Client shall accept only trustworthy certificates, the default acceptance of all certificates has to be deactivated as follows:
    client.Security.AutoAcceptUntrustedCertificates = False
  • As soon as the default acceptance of all certificates has been deactivated, a custom checking of certificates should be considered:
    AddHandler client.CertificateValidationFailed, AddressOf HandleCertificateValidationFailed
    ...
    Private Sub HandleCertificateValidationFailed( _
            ByVal sender As Object, _
            ByVal e As OpcCertificateValidationFailedEventArgs)
        If e.Certificate.SerialNumber = "..." Then
            e.Accept = True
        End If
    End Sub
  • If the Server certificate is categorized as untrusted it can be manually declared trusted. Therefore it has to be saved in the TrustedPeerStore:
    'In context of the event handler the sender is an OpcClient.
    Dim client = CType(sender, OpcClient)
     
    If Not client.CertificateStores.TrustedPeerStore.Contains(e.Certificate) Then
        client.CertificateStores.TrustedPeerStore.Add(e.Certificate)
    End If

Configuration via XML

If the Client shall also be configurable via XML, the configuration of the Client can be directly loaded either from a specific or from a random XML file.

Using a certain XML file it has to show the following default XML tree:

<?xml version="1.0" encoding="utf-8" ?>
<OpcApplicationConfiguration
    xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd"
    schemaLocation="AppConfig.xsd">

In case that a random XML file shall be used for the configuration, a .config file has to be created that refers to the XML file the configuration of the Client shall be loaded from. The following section shows which entries the .config file therefore has to contain and which structure the XML file has to show.

Creating and preparing the App.config of the application:

  1. Add an App.config (if not already existing) to the project
  2. Insert the following configSections element underneath the configuration element:
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <configSections>
        <section name="Opc.UaFx.Client"
                 type="Opc.Ua.ApplicationConfigurationSection,
                       Opc.UaFx.Advanced,
                       Version=2.0.0.0,
                       Culture=neutral,
                       PublicKeyToken=0220af0d33d50236" />
      </configSections>
  3. Also insert the following Opc.UaFx.Client element underneath the configuration element:
      <Opc.UaFx.Client>
        <ConfigurationLocation xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd">
          <FilePath>ClientConfig.xml</FilePath>
        </ConfigurationLocation>
      </Opc.UaFx.Client>
  4. The value of the FilePath element can show towards a random file path where the XML configuration file to be used can be found. The value depicted here would refer to a configuration file lying next to the application.
  5. Save the changes to the App.config

Creating and preparing the XML configuration file:

  1. Create an XML file with the file name used in the App.config and save the used path.
  2. Insert the following default XML tree for XML configuration files:
    <?xml version="1.0" encoding="utf-8" ?>
    <OpcApplicationConfiguration
        xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd"
        schemaLocation="AppConfig.xsd">
  3. Save the changes to the XML file.

This is how you prepare your OPC UA Client application for the use in productive environments.

Application certificates - Using a concrete certificate
For productive use don't use a certificate automatically generated by the Framework.

If you already have an appropriate certificate for your application you can load your PFX-based certificate from any random Store and assign it to the Client instance via the OpcCertificateManager:

Dim certificate = OpcCertificateManager.LoadCertificate("MyClientCertificate.pfx")
client.Certificate = certificate

Note that the application name has to be included in the certificate as “Common Name” (CN) and has to match with the value of the AssemblyTitle attribute:

<Assembly: AssemblyTitle("<Common Name (CN) in Certificate>")>

If that isn't the case you have to set the name used in the certificate via the ApplicationName property of the Client instance. If the “Domain Component” (DC) part is used in the certificate the value of the ApplicationUri property of the application has to show the same value:

client.ApplicationName = "<Common Name (CN) in Certificate>"
client.ApplicationUri = New Uri("<Domain Component (DC) in Certificate>")

If you don't already have an appropriate certificate you can use as an application certificate for your Client you should at least create and use a self-signed certificate via the Certificate Generator of the OPC Foundation. The Certificate Generator (Opc.Ua.CertificateGenerator.exe) included in the SDK of the Framework is opened as follows:

Opc.Ua.CertificateGenerator.exe -sp . -an MyClientAppName

The first parameter (-sp) sets saving the certificate in the current directory. The second parameter (-an) sets the name of the Client application using the application certificate. Replace “MyClientAppName” by the name of your Client application. Note that the Framework for chosing the application certificate uses the value of the AssemblyTitle attribute and therefore the same value as stated in this attribute is used for “MyClientAppName”. In alternative to the value in the AssemblyTitle attribute the value used in the application certificate can be set via the ApplicationName property of the Client instance:

client.ApplicationName = "MyDifferentClientAppName"

It is important that either the value of the AssemblyTitle attribute or the value of the ApplicationName property equals the value of the second parameter (-an). If you want to set further properties of the certificate as, for example, the validity in months (default 60 months) or the name of the company or the names of the domains the Client will be working on, call the generator with the parameter “/?” in order to receive a list of all further / possible parameter values:

Opc.Ua.CertificateGenerator.exe /?

After the Certificate Generator was opened with the corresponding parameters, the folders “certs” and “private” are in the current directory. Without changing the names of the folders and the files, copy both folders in the directory that you set as Store for the application certificates. By default that is the folder “Trusted” in the folder “CertificateStores” next to the application.

If you have set the parameter “ApplicationUri” (-au) you have to set the same value on the ApplicationUri property of the Client instance:

client.ApplicationUri = New Uri("<ApplicationUri>")

Configuration Surroundings - All files necessary for an XML-based configuration

If the application shall be configurable through a random XML file referenced in the App.config, App.config has to be in the same directory as the application and hold the name of the application as a prefix:

<MyClientAppName>.exe.config

If the application is configured through a (certain) XML file, ensure that the file is accessible for the application.

Licensing

The OPC UA SDK comes with an evaluation license which can be used unlimited for each application run for 30 minutes. If this restriction limits your evaluation options, you can request another evaluation license from us.

Just ask our support (via support@traeger.de) or let us consult you directly and clarify open questions with our developers!

After receiving your personalized license key for OPC UA Client development it has to be committed to the framework. Hereto insert the following code line into your application before accessing the OpcClient class for the first time. Replace <insert your license code here> with the license key you received from us.

Opc.UaFx.Client.Licenser.LicenseKey = "<insert your license code here>"

If you purchased a bundle license key for OPC UA Client and Server development from us, it has to be committed to the framework as follows:

Opc.UaFx.Licenser.LicenseKey = "<insert your license code here>"

Additionally you receive information about the license currently used by the framework via the LicenseInfo property of the Opc.UaFx.Client.Licenser class for Client licenses and via the Opc.UaFx.Licenser class for bundle licenses. This works as follows:

Dim license As ILicenseInfo = Opc.UaFx.Client.Licenser.LicenseInfo
 
If license.IsExpired Then
    Console.WriteLine("The OPA UA Framework Advanced license is expired!")
End If

Note that a once set bundle license becomes ineffective by additionally committing a Client license key!

In the course of development/evaluation, it is mostly irrelevant whether the test license or the license already purchased is being used. However, as soon as the application goes into productive use, it is annoying if the application stops working during execution due to an invalid license. For this reason, we recommend implementing the following code snippet in the Client application and at least executing it when the application is started:

#If DEBUG Then
    Opc.UaFx.Client.Licenser.FailIfUnlicensed()
#Else
    Opc.UaFx.Client.Licenser.ThrowIfUnlicensed()
#End If

You can receive further information about licensing, purchase or other questions directly on our product page at: opcua.traeger.de/en.

1) The OPC UA Wrapper Server is automatically started by the OPC UA Client or reused if it already exists. The OPC UA Wrapper Server 'wraps' the accesses of the OPC UA Client and sends them to the OPC Classic Server the answers received from the OPC Classic Server are sent to the OPC UA Wrapper Server as OPC UA responses to OPC UA Client back again.