High Performance OPC UA Server SDK
1.7.1.383
|
This example shows how dynamic instance creation can be customized by implementing different callbacks.
The files used in this lesson are also included in the SDK packages in the examples folder at server_collection/instance. To try the example, the server must be started as:
./uaserver_collection -a instance
Afterwards it is possible to connect with e.g. UAExpert and view the types and instances in the live address space of the server.
Files used in this lesson:
In this example different sample instances of different types are dynamically created, each focusing on a different callback and showing how this callback influences the resulting instance.
For the callbacks a data-driven approach is used, this may seem a bit over-engineered in these small samples, but for more complex types it is highly recommended as it makes the code much cleaner and better to understand.
In the following sections first the provider code and the direct call for creating instances is shown, the later sections cover the individual sample instances and their callbacks.
Before creating the instances, the provider is initialized, this is rather straightforward: The types which are going to be instantiated in this example are located in instance_examples.xml, this file is converted to instance_examples.bin and loaded by the provider code:
The instances are created in the same namespace, so a few additional nodes are reserved. Regarding the instance creation it doesn't matter if the instances are from the same namespace as the types, it is also possible to have the type in a static namespace and create the instances in a different dynamic namespace. After loading the namespace, the function to create the instances is called.
The structure of the resulting instance is mostly determined by callbacks, the direct call for the instance creation is similar for all samples. So in this example a struct is defined with the differing parameters for the instances:
This struct is used after creating the ua_instance_ctx to set the callbacks and for the Nodeid of the type and the name (Browsename and Displayname) of the instance. For the Nodeid of the instance a global variable is used, its initial value determines the Nodeid of the first instance, the nodes of the instance receive consecutive numeric Nodeids and after instance creation the variable is updated with the next free numeric identifier. This way all nodes use a continuous number range, to find a specific instance (or member) the translate functionality can be used as all are added directly below the ObjectsFolder with their name. For a server internal translate ua_node_translate_va or ua_node_translate_array may be used.
Creating multiple instance is easy now, each instance information is in an array of the above structs and by iterating over the array the instances are created. In this function also the valuestore for the values of the first sample instance is initialized:
The type for the first sample instance has two UA Variables, each with an additional EngineeringUnits Property:
The goal is to create an instance of this type with all nodes having meaningful values. For the UA Variables the values already exist in the code as global variables. To connect these to the UA Variables in a data-driven approach, a struct is defined with a unique mapping of the Browsenames to pointers to the variables:
In this sample the text part of the Browsename is sufficient for a unique mapping, however that may not always be the case e.g. if the mapping would include the EngineeringUnits. In such cases another mapping is needed to be unique, a method that always works is using the Nodeids of the type nodes (instance declarations) as these are always unique and also passed to the callback as declaration_node.
To finally connect the C variables to the UA Variables, the post create callback is implemented, it is always called when a new node is created at the instance within ua_instance_new. In the callback the Browsename of the newly created instance node is used to find the matching array entry with the pointer and bring both together in the valuestore initialized in the previous section:
Goal achieved, when starting the server and navigating to Root->Objects->SampleA with the UAExpert all member nodes have values. But what about the EngineeringUnits? Also the EngineeringUnits have values: the EngineeringUnits at the SampleAType already have values in the XML-file, so when the namespace was loaded the values were added to the g_staticstore and when the instance nodes were created the store and valueindex for the existing values were also set at the new node.
This is rather comfortable, but it comes with a catch: the value is basically hard-linked, so multiple nodes point to the same value and changing the value at one node would result in changing the value for all nodes. To mitigate this danger, values are only hard-linked if the type node (and thus also the instance node) is read-only. If the value still needs to be changed only on a single node, a new value must be created and connected with the new node via a valuestore like in the code above.
The valuestore used in this sample is a pointerstore, however there is variety of valuestores implemented in the SDK and users may also implement their own. So depending on the situation the most suitable one may be used.
Also note that in this sample the post create callback is only used to connect the node with a value, which basically means modifying the value and storeindex of the new node, however the callback may also be used to alter different attributes of the new node like permissions. When doing so, it must be taken care the new node is still compatible to its instance declaration.
The type for this sample instance is similar to the previous one with two nodes describing the features Volume and Weight of the Object, but additionally a Method to calculate the density from these features and a Placeholder for another feature is added(the Method is not implemented though, Methods are described in Lesson 3: Creating Methods):
The instance created from that type should have the following customizations:
<OtherFeature>
is a MandatoryPlaceholder, hence the node cannot be created at the instance but instead one or more other nodes must be created, describing additional features in this case. So instead of the placeholder a node for the color of the Object is to be created.To achieve these customizations it is necessary to influence the instance creation before the nodes are being created, thus the pre create callback must be implemented. This callback is called before a node is to be created and its return value indicates how the node should be handled. Again a struct is defined to identify the nodes and select the desired behavior:
This achieves the above customizations:
<OtherFeature>
node is also ignored, but when encountering the node the function to create the color node is called.The implementation of the callback itself again looks up the entry for the current node, calls its own callback if it is set and returns the action how to handle the node:
The function to create the color node just calls ua_node_create_with_attributes to create a new UA Variable node, a real application would need to set some further attributes, especially the value:
Watching the resulting instance with the UAExpert shows the EngineeringUnits are missing and <OtherFeature>
is replaced with Color:
Clicking the calculateDensity node shows the Nodeid is not from the 2000s range like the other members, but it has the same Nodeid like at the type, meaning these are the identical nodes:
In this sample the rules how to handle nodes are based on the node names and previous knowledge of the modeling rules, e.g. the EngineeringUnits can only be ignored because these are optional. Another option would be to act directly based on the modeling rules such as "copy all mandatory nodes and ignore everything else", however in the case of MandatoryPlaceholders this would not yield a conforming result as placeholders mostly require domain specific knowledge like in this sample. Also note the SDK does not limit instance creation to conforming Objects, there is still responsibility left to the application.
This is an advanced technique, so a bit of background helps before showing the implementation of this sample.
Every UA Variable node needs a TypeDefinition, this is a Reference of type HasTypeDefinition from the node to a VariableType. Depending on the VariableType the TypeDefinition may restrict the DataType of a UA Variable but can also enforce further child nodes, as all mandatory components of the VariableType also need an equivalent on the UA Variable. Mostly these further nodes are Properties but can also be conventional UA Variables (which have TypeDefinitions themselves).
When it comes to instancing then in theory there shouldn't be a problem: the UA Variables that are part of a ObjectType need to comply to the same rules as instances, so these are required to have all the child nodes enforced by the TypeDefinition and when creating the instance these child nodes only need to be copied (or referenced) at the instance.
In practice however there might arise issues:
The technique to solve these issues is what we call sub-instancing: when a TypeDefinition is encountered while instancing, a new (sub-)instance is created with the UA Variable as initial node and its VariableType as type. To control the behavior of sub-instancing and select a TypeDefinition, another callback can be implemented which this sample focuses on.
This sample uses a simple type with three features and no Properties:
In the previous samples the TypeDefinition always was BaseDataVariableType which has no restrictions on the DataType and no (direct) Properties or other child nodes, so when using this TypeDefinition and creating an instance you basically don't have to care about the TypeDefinition. In this sample however all three features have DataItemType as TypeDefinition and furthermore the TypeDefinition for one of them is be changed at the instance.
Again there is a struct with the Browsenames, this time it maps the nodes to the TypeDefinitions they use for sub-instancing:
The TypeDefinition callback looks up the struct entry and searches for the namespace zero node given by their numeric identifier to return the result:
That has the following effects:
Therefore the resulting instance looks like this:
The pre and post create callbacks from the previous samples while sub-instancing are called for the Properties as well, so it is possible to exactly control which nodes should be created and to connect them to a value or apply some further modifications. In this sample all optional and mandatory nodes should be created as copy, hence this pre create callback is used:
As the chosen TypeDefinitions also place further restrictions on the DataType, it is worth a look: the DataItemType only permits subtypes of Numeric, the MultiStateDiscreteType further restricts the DataType to unsigned integers. All of the created UA Variables at the instance conform to these restrictions, however these are not enforced by the SDK hence the application needs to pay attention to create only valid instances.
These samples have shown how a dynamically created instance can be heavily customized using the respective callbacks. Each sample is focused on a single callback, however many applications need to implement at least two of them:
The TypeDefinition callback is a rather advanced feature and may only be needed in rare cases. For more control regarding non-hierarchical references it is furthermore possible to implement the ua_instance_reference_cb which is not discussed in this example.