.NET Based OPC UA Client/Server SDK  3.3.3.537
Toolkit

Overview

The BaseNodeManager implements all of the manager interfaces and provides access to a set of in-memory Nodes. It also provides functions that can be overloaded by a subclass to access data for some or all of the in-memory Nodes or to generate events based on application specific logic.

Data Access, Handle Types and the IIOManager

The BaseNodeManager uses UInt32 values called handle types to control how Node attributes are accessed. The user can set the handle type when the Node is created (via one of the Create<NodeClass> overloads) or with the SetNodeAttributeConfiguration or SetVariableConfiguration methods. The following handle types are supported by the BaseNodeManager:

Internal

The attribute value is stored in the memory cache maintained by the BaseNodeManager. Any writes will cause the ReportDataChanges method to be called which updates any MonitoredItems.

InternalPolled

The attribute value is linked by reflection to a property of a class instance in memory. The association is created with the LinkModelToNode method and the caller is responsible for keeping a reference to the linked instance. If this attribute value is subscribed, the BaseNodeManager will periodically poll the value and report any data changes.

InternalPush

The attribute value is stored in the memory cache maintained by the BaseNodeManager. The type is typically used by the BindModel method. Changes on the model are pushed to the node memory. External writes are re-directed to the model.

ExternalPolled

The attribute value is stored in an external system. The BaseNodeManager will return an error if the attribute value is accessed and it is up to the subclass to override the Read/Write methods and provide the logic. If the node is subscribed to, then the BaseNodeManager will periodically call the Read method and report any data changes.

ExternalPush

The attribute value is stored in an external system. The BaseNodeManager will return an error if the attribute value is accessed and it is up to the subclass to override the Read/Write/StartMonitoring/StopMontoring methods and provide the logic required.

VendorDefined
Subclasses can create additional handle types starting with this value. The BaseNodeManager treats all handle types greater than ExternalPush as ExternalPush.

The InternalPolled and InternalPush handle types allow the use of reflection to link user defined classes to an information model. This requires that the class properties are decorated with the UaInstanceDeclaration attribute such as shown in the following code snippet:

[UaTypeDefinition(NodeId=ObjectTypes.FileType, NamespaceUri=Namespaces.OpcUa)]
public partial class FileModel
{
[UaInstanceDeclaration(NamespaceUri = Namespaces.OpcUa)]
public ushort OpenCount { get; set; }
[UaInstanceDeclaration(NamespaceUri = Namespaces.OpcUa)]
public ulong Size { get; set; }
[UaInstanceDeclaration(NamespaceUri = Namespaces.OpcUa)]
public bool UserWriteable { get; set; }
}

This class could be associated with any Object Node with a TypeDefinition FileType. The value of the OpenCount Variable Node would be mapped to the OpenCount property.

If an attribute value is a LocalizedText, then the user can tell the SDK to automatically translate the text with the ResourceManager by specifying a value for the Key property. The translation occurs when the value is added to the queue of a MonitoredItem.

The following methods can be overridden by the subclass to customize I/O operations:

Read (<single nodes>)

This method is called when the ServerManager calls BeginRead. The overridden implementation must not block and returns null if the Node should be processed as part of a transaction.

Read (<multiple nodes>)

This method is called when the ServerManager calls FinishDataTransaction. This method can block and must invoke the ServerManager completion callback for each operation passed in.

Write (<single nodes>)

This method is called when the ServerManager calls BeginWrite. The overridden implementation must not block and returns null if the Node should be processed as part of a transaction.

Write (<multiple nodes>)

This method is called when the ServerManager calls FinishDataTransaction. This method can block and must invoke the ServerManager completion callback for each operation passed in.

StartDataMonitoring (<single nodes>)

This method is called when the Subscription calls BeginStartDataMonitoring. The overridden implementation must not block and returns null if the Node should be processed as part of a transaction.

StartDataMonitoring (<multiple nodes>)

This method is called when the ServerManager calls FinishDataTransaction. This method can block and must invoke the ServerManager completion callback for each operation passed in.

StopDataMonitoring (<single nodes>)

This method is called when the Subscription calls BeginStopDataMonitoring. The overridden implementation must not block and returns null if the Node should be processed as part of a transaction.

StopDataMonitoring (<multiple nodes>)

This method is called when the ServerManager calls FinishDataTransaction. This method can block and must invoke the ServerManager completion callback for each operation passed in.

ModifyDataMonitoring (<single nodes>)

This method is called when the Subscription calls BeginModifyDataMonitoring. The overridden implementation must not block and returns null if the Node should be processed as part of a transaction.

ModifyDataMonitoring (<multiple nodes>)

This method is called when the ServerManager calls FinishDataTransaction. This method can block and must invoke the ServerManager completion callback for each operation passed in.

SetDataMonitoringMode (<single nodes>)

This method is called when the Subscription calls BeginSetDataMonitoringMode. The overridden implementation must not block and returns null if the Node should be processed as part of a transaction.

SetDataMonitoringMode (<multiple nodes>)
This method is called when the ServerManager calls FinishDataTransaction. This method can block and must invoke the ServerManager completion callback for each operation passed in.

IEventManager and the Notifier Hierarchy

The BaseNodeManager maintains an in memory representation of the notifier hierarchy. It is optimized to allow for fast propagation of events up the hierarchy. The AddNotifier method can be used to add new notifiers to this cache; however, this is also done automatically whenever a HasNotifier reference is added via AddNode or AddReference methods (or via any of the Create<NodeClass> methods).

This cache allows applications to create events once, set the source and call the BaseNodeManager ReportEvent method. The SDK will take care of walking up the hierarchy and reporting the event to every currently active MonitoredItem.

An event created with a Server is an instance of a GenericEvent class. This class requires an instance of FilterManager (a property on the ServerManager instance). The FilterManager assigns handles to all known sequences of BrowseNames (also called a BrowsePath). Events that are created rarely can be populated with the Set methods that take a hardcoded BrowsePath as an argument as shown in the following code snippet:

GenericEvent e = new GenericEvent(Server.FilterManager);
"DeviceFailedMessage",
"en-US",
"The device has failed for with error {0}.",
errorCode);
e.Initialize(
null,
ObjectTypeIds.DeviceFailureEventType,
sourceNode,
sourceName,
message);
e.Set("DeviceId", deviceId);
e.Set("LastGoodStatus", lastGoodStatus);
e.Set("LastGoodStatus/Timestamp", timestamp);

The first argument of the LocalizedText constructor is a key that is used by the ResourceManager to look up translations of the default text. If no translation is found, the default text is used. This allows the framework to automatically translate the LocalizedText into the locales required by different MonitoredItems.

The Initialize method is used to set the event fields required to process the event within the SDK (EventId, EventType, SourceNode and Time). If any mandatory field is set to null, a suitable default is chosen. If the four required fields need to be overridden, the properties on the GenericEvent object need to be set explicitly. Calling the Set method will mean the SDK will not see the changes.

If an event is created over and over again, the user should register the BrowsePath with the FilterManager.CreateFieldHandle method and save the handle returned. When a new instance of the event is created, the field values should be set using this saved handle. The code generated by the UaModeler includes code to save and use these handles when filling in an event.

The BaseNodeManager also provides methods which are called when operations are performed on notifiers in the hierarchy that can affect the state of nodes below it. These methods are summarized in the following:

OnMonitoringStarted

Called when a MonitoredItem is created or moves from the Disabled state for a notifier at or above the notifier passed to the call.

OnMonitoringModified

Called when a MonitoredItem is modified for a notifier at or above the notifier passed to the call.

OnMonitoringStopped

Called when a MonitoredItem is deleted or moves to the Disabled state for a notifier at or above the notifier passed to the call.

OnConditionRefresh
Called when ConditionRefresh is called for a MonitoredItem for a notifier at or above the notifier passed to the call.

The SDK also provides helper classes for all of the OPC UA defined event types such as AlarmType.

The classes can be used to store “templates” for events that can be raised over and over. The following snippet initializes one of these classes (by convention the class name is the same as the event type except “Type” is replaced with “Model”):

// create an in memory object that can be used to manage the alarm.
ExclusiveLevelAlarmModel alarm = new ExclusiveLevelAlarmModel();
alarm.NodeId = alarmId;
alarm.EventType = ObjectTypeIds.ExclusiveLimitAlarmType;
alarm.SourceNode = controllerId;
alarm.SourceName = block.Name;
alarm.Message = "Controller is not maintaining temperature.";
alarm.Severity = (ushort)EventSeverity.Low;
alarm.ConditionName = "TemperatureAlarm";
alarm.ConditionClassId = ObjectTypeIds.ProcessConditionClassType;
alarm.ConditionClassName = BrowseNames.ProcessConditionClassType;
alarm.Retain = false;
alarm.EnabledState.Value = ConditionStateNames.Enabled;
alarm.EnabledState.Id = true;
alarm.AckedState.Value = ConditionStateNames.Acknowledged;
alarm.AckedState.Id = true;
alarm.ActiveState.Value = ConditionStateNames.Inactive;
alarm.ActiveState.Id = false;
alarm.SuppressedOrShelved = false;
alarm.HighLimit = 35;
alarm.LowLimit = 15;
alarm.InputNode = temperatureVariable;

The following code snippet will raise an event based on the in memory object:

GenericEvent e = alarm.CreateEvent(Server.FilterManager);
ReportEvent(alarm.SourceNode, e);

These convenience classes can be created for a user defined information model with the UaModeler tool that can be downloaded from the Unified Automation website.

IMethodManager and IMethodDispatcher

The BaseNodeManager implements the Call service for Objects which belong to the NodeManager. The Method is expected to be in the same NodeManager or the Method defined on the TypeDefinition for the Object.

When the GetMethodHandle method is called, a delegate for the method implementation needs to be found. First, the BaseNodeManager looks in the UserData associated with the Object Node (set by calling the SetNodeUserData method). If the UserData supports the IMethodDispatcher interface, then GetMethodDispatcher is called on the UserData. Otherwise, the GetMethodDispatcher method on the BaseNodeManager is called. If GetMethodDispatcher returns a null, then a Bad_NotImplemented error will be returned to the Client.

When the FinishCallTransaction method is called by the ServerManager, the BaseNodeManager will call the CallMethod method for each operation in the transaction. This method validates the input arguments by checking the number and the data type. It also creates an array of output arguments which are initialized with the default values for the data type.

The BaseNodeManager finds the input and output arguments by reading the value of the InputArguments and OutputArguments properties of the Method Node. For better performance these properties should be stored in memory.

If the input arguments are valid, the delegate returned by GetMethodDispatcher is called. If this method throws an exception, it is caught and an error status is returned for the operation. The implementer of the method does not need to check the number and type of arguments since the caller has already done this. Other checks may be required depending on the method call.

The following code snippet illustrates how to implement the IMethodDispatcher on an Object that can be set as the UserData for a Node.

public class FileModel : IMethodDispatcher
{
// Gets the method dispatcher.
public virtual CallMethodEventHandler GetMethodDispatcher(RequestContext context,
NodeId objectId,
NodeId methodId)
{
return DispatchMethod;
}
// Dispatches the method.
public virtual StatusCode DispatchMethod(RequestContext context,
MethodHandle methodHandle,
IList<Variant> inputArguments,
List<StatusCode> inputArgumentResults,
List<Variant> outputArguments)
{
// use the NodeId of the method defined on the TypeDefinition as a constant.
if (methodHandle.MethodDeclarationId == MethodIds.FileType_Open.ToNodeId(context.NamespaceUris))
{
uint mode = inputArguments[0].ToByte();
uint fileHandle = 0;
// implementation can throw StatusExceptions to report errors back to the client.
Open(context, mode, out fileHandle);
outputArguments[0] = fileHandle;
return StatusCodes.Good;
}
if (methodHandle.MethodDeclarationId == MethodIds.FileType_Close.ToNodeId(context.NamespaceUris))
{
uint fileHandle = inputArguments[0].ToUInt32();
Close(context, fileHandle);
return StatusCodes.Good;
}
return StatusCodes.BadNotImplemented;
}
}

The SDK includes an implementation of IMethodDispatcher for UA defined ObjectTypes such as AlarmTypes. This implementation invokes methods on an interface (called I<TypeName>Methods) containing all methods defined for the ObjectType.

These standard IMethodDispatcher implementations can be created for a user defined information model with the UaModeler tool that can be downloaded from the Unified Automation website.

INodeManagementManager and ImportUaNodeSet

The BaseNodeManager allows users to create or delete nodes and references using the INodeManagementManager interface. In addition, BaseNodeManager provides simplified helper functions that create instances for each NodeClass (the Create<NodeClass> methods).

When creating Objects or Variables, the BaseNodeManager automatically creates all of the mandatory instances required by the TypeDefinition. The user can request that optional instances are created as well by setting the OptionalBrowsePaths property. The BaseNodeManager determines the structure of the TypeDefinition by browsing the Server address space using the InternalClient object. It caches any results found to improve the performance of subsequent instantiations of the same TypeDefinition.

When Objects or Variables are instantiated, the child Nodes need NodeIds assigned. The NodeIdGenerationSettings property controls the behavior. There are basically two modes that depend on whether a string identifier was passed in as the RequestedNodeId. If the RequestedNodeId has a string, then the NodeIds of the components are constructed by appending the delimiter specified in the settings and the name portion of the BrowseName. If the RequestedNodeId has a type other than string, then new identifiers are created by generating Guids or incrementing a counter in the settings.

Nodes can also be added to the BaseNodeManager with the ImportUaNodeSet method. This method reads an XML document from a file or a stream that conforms to the UANodeSet schema. During import any reverse references are added, any notifiers are indexed and any new namespace URIs are added to the Server’s namespace table. This method will also register the BaseNodeManager doing the import as the NodeManager for URIs used by the NodeIds of Nodes defined in the document.

ServerInternalClient and IAdvancedNodeManager

ServerInternalClient is a class that calls the same methods that the ServerManager calls, however, it provides an API that can be used by other components of the Server process. The primary APIs are as follows:

The caller passes a delegate into the CreateDataMonitoredItems method which is called whenever a data change occurs so there is no need for the Subscribe and Publish services.

The ServerInternalClient also provides the following helper methods:

GetNodeMetadata

Returns attributes used during browsing. Includes a mask that allows the caller to specify exactly which attributes are required.

GetNodeAttributes

Returns all attributes for a given Node.

ReadAttribute< T >

Reads the value of one attribute for a Node. The default value is returned if an error occurs or if the value cannot be cast to the specified type.

ReadValue< T >

Reads the value of a component variable for a Node. The component is specified with the BrowseName or an array of BrowseNames. The default value is returned if an error occurs or if the value cannot be cast to the specified type.

WriteAttribute

Writes the value of one attribute for a Node.

WriteValue
Writes the value of a component variable for a Node. The component is specified with the BrowseName or an array of BrowseNames.

The helper methods use the Read, Write, Browse and Translate methods, however, this is often an inefficient way to get the information for some NodeManagers (such as NodeManagers which inherit from BaseNodeManager). For this reason the SDK defines the IAdvancedNodeManager interface which allows a NodeManager to directly implement these functions. If the IAdvancedNodeManager interface is not defined on the NodeManager associated with the Node, then the default method is used.

Locking

The BaseNodeManager provides a InMemoryNodeLock [LINK TO CLASS DOCU]. The lock is needed to synchronize access to the in-memory-node between the SDK and your implementation.

  • Locking is required when you read or write any property of the in-memory-node (like Value, AccessLevel, Description)
  • Locking is already encapsulated when creating or deleting in-memory-nodes, so your code does not need to call the lock

It is not required to lock the in-memory-nodes while you are initializing (Startup) your NodeManager, but after startup you have to lock them when you access the nodes. Example:

lock (InMemoryNodeLock)
{
node.Description = description;
}

As the lock is locking all in-memory-nodes of the whole NodeManager, you should keep the lock as short as possible, and do not have long-lasting operations (like file access) while keeping the lock. If you use generated classes, or helper classes of the SDK representing OPC UA nodes (classes ending with “Model”) you need to lock them, before changing values or calling methods changing the state (like in case of the ConditionModel). In this case, you use the object itself as locking object. Example:

lock (model)
{
model.TemperatureSetPoint = Math.Max(Math.Min(setPoint, m_boiler1.TemperatureSensor.Temperature.EURange.High), m_boiler1.TemperatureSensor.Temperature.EURange.Low);
}

Note that for those generated classes, only accessing of the values is locked. Accessing the meta data (like AccessLevel or Description) is still done via the in-memory-nodes.