High Performance OPC UA Server SDK  1.7.1.383
Instance Examples

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:

Introduction

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.

Provider Initialization

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:

int provider_instance_init(struct uaprovider *ctx)
{
const char *filename = "instance_examples.bin";
struct ua_addressspace_config config;
int ret;
config.nsidx = UA_NSIDX_INVALID; /* auto assign */
/* reserve additional memory for dynamic instances */
config.max_variables = 100;
config.max_objects = 100;
config.max_methods = 100;
config.max_references = 100;
config.max_strings = 100;
/* staticstore for values loaded from the bin file */
ret = ua_staticstore_init(&g_staticstore, 0);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: ua_staticstore_init() failed with error=%i\n", __func__, ret);
return ret;
}
TRACE_INFO(TRACE_FAC_APPLICATION, "Creating dynamic addressspace for %s\n", filename);
ret = ua_addressspace_load_file(filename, &g_staticstore, &config);
if (ret < 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "Loading %s failed with error=%i\n", filename, ret);
return ret;
}
g_nsidx = (uint16_t)ret;
TRACE_INFO(TRACE_FAC_APPLICATION, "Loaded NS from binary file '%s', nsidx=%u\n", filename, g_nsidx);
ret = uaprovider_register_nsindex(ctx, g_nsidx);
if (ret < 0) return ret;
ret = create_all_instances();
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: create_all_instances failed with error=%i\n", __func__, ret);
return ret;
}
ctx->cleanup = provider_instance_cleanup;
return ret;
}

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.

Instance Creation

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:

struct instance_info {
const char *name;
uint32_t type_identifier;
};

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.

static int create_instance(const struct instance_info *info)
{
ua_node_t type_node = ua_node_find_numeric(g_nsidx, info->type_identifier);
struct ua_instance_ctx *ctx = NULL;
struct ua_nodeid instance_id;
int ret;
if (ctx == NULL) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: ua_instance_ctx_create failed\n", __func__);
ret = UA_EBADNOMEM;
goto error;
}
/* set callbacks */
ua_instance_set_pre_create2_cb(ctx, info->pre_create2_cb);
ua_instance_set_post_create_cb(ctx, info->post_create_cb);
ua_instance_set_typedefinition_cb(ctx, info->type_def_cb);
ua_instance_set_reference_cb(ctx, info->ref_cb);
/* the instance will be automatically added to the parent, if set */
ua_instance_set_parent(ctx, ua_node_find0(UA_ID_OBJECTSFOLDER), ua_node_find0(UA_ID_ORGANIZES));
/* nodeid for the new instance */
ua_nodeid_set_numeric(&instance_id, g_nsidx, g_next_identifier);
/* actually create the new instance */
ret = ua_instance_new(ctx, &instance_id, type_node, g_nsidx, info->name, info->name);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: ua_instance_new failed with error=%i\n", __func__, ret);
goto error;
}
/* the identifier after the last created node is used for the next instance */
g_next_identifier = ua_instance_get_last_numeric_id(ctx) + 1;
return 0;
error:
if (ctx != NULL) ua_instance_ctx_delete(ctx);
return ret;
}

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:

struct instance_info g_instances[] = {
{"SampleA", 1002, NULL, sampleA_post_create_cb, NULL, NULL},
{"SampleB", 1003, sampleB_pre_create2_cb, NULL, NULL, NULL},
{"SampleC", 1004, sampleC_pre_create2_cb, NULL, sampleC_typedef_cb, NULL},
};
static int create_all_instances(void) {
unsigned int i;
int ret;
ret = ua_pointerstore_init(&g_sampleA_store, 10, g_sampleA_store_values, countof(g_sampleA_store_values));
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: ua_pointerstore_init failed with error=%i\n", __func__, ret);
return ret;
}
for (i = 0; i < countof(g_instances); i++) {
ret = create_instance(&g_instances[i]);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: creating instance %s failed error=%i\n", __func__, g_instances[i].name, ret);
return ret;
}
}
return 0;
}

SampleA: Populating the Value Attribute

The type for the first sample instance has two UA Variables, each with an additional EngineeringUnits Property:

SampleAType.png
SampleAType

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:

struct sampleA_node_info {
const char *browsename;
void *value;
};
static uint32_t g_sampleA_volume = 67;
static double g_sampleA_weight = 1736.3;
static const struct sampleA_node_info g_sampleA_nodes[] = {
{"Volume", &g_sampleA_volume},
{"Weight", &g_sampleA_weight},
};

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:

static int sampleA_post_create_cb(
struct ua_instance_ctx *ctx,
ua_node_t new_node,
ua_node_t declaration_node,
enum ua_nodeclass nc)
{
struct ua_qualifiedname bn;
unsigned int i;
int ret;
UA_UNUSED(ctx);
UA_UNUSED(declaration_node);
UA_UNUSED(nc);
ret = ua_node_get_browsename_const(new_node, &bn);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: ua_node_get_browsename_const failed with error=%i\n", __func__, ret);
return ret;
}
for (i = 0; i < countof(g_sampleA_nodes); i++) {
const struct sampleA_node_info *node_info = &g_sampleA_nodes[i];
if (ua_string_compare_const(&bn.name, node_info->browsename) != 0) {
continue;
}
/* connect pointer to the value with the newly created node */
ret = ua_pointerstore_register_node(&g_sampleA_store, new_node, node_info->value, -1);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: ua_pointerstore_register_node failed with error=%i\n", __func__, ret);
return ret;
}
break;
}
return 0;
}

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.

SampleB: Deciding which Nodes to create

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

SampleBType.png
SampleBType

The instance created from that type should have the following customizations:

  • The nodes Volume and Weight should be created like in the previous sample, but their EngineeringUnits are optional this time and should not become part of the instance.
  • The node <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.
  • Usually nodes are replicated on instances by creating a copy, however there is another way: Instead of creating a new node and drawing a reference from the Object node to the new, it is also possible to draw the reference directly to the original (instance declaration) node without creating a copy. This is again a kind of hard-linking with the same node being part of multiple instances and the same disadvantage: when the node is changed or a reference is added this applies to all instances and the type, so it is not possible to have instance specific customizations like different values or permissions. For Methods these must be identical at the type and the Object anyway and it is also possible to use either the Method at the Object or type for the Call service, therefore the calculateDensity Method should only be referenced and not copied.

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:

struct sampleB_node_info {
const char *browsename;
enum ua_instance_node_action node_action;
int (*action_cb) (ua_node_t parent);
};
static const struct sampleB_node_info g_sampleB_nodes[] = {
{"Volume", UA_INSTANCE_NODE_COPY, NULL},
{"Weight", UA_INSTANCE_NODE_COPY, NULL},
{"EngineeringUnits", UA_INSTANCE_NODE_IGNORE, NULL},
{"<OtherFeature>", UA_INSTANCE_NODE_IGNORE, create_color_node},
{"calculateDensity", UA_INSTANCE_NODE_REFERENCE, NULL},
};

This achieves the above customizations:

  • For Volume and Weight the nodes are copied, but their EngineeringUnits are ignored.
  • The <OtherFeature> node is also ignored, but when encountering the node the function to create the color node is called.
  • For the calculateDensity node only a reference to the original node is drawn. Note there is no entry for its child node OutputArguments as a referenced node is taken with its complete subtree as-is, so the node is not newly created and no pre create callback is made.

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:

enum ua_instance_node_action sampleB_pre_create2_cb(
struct ua_instance_ctx *ctx,
{
struct ua_qualifiedname bn;
unsigned int i;
int ret;
UA_UNUSED(ctx);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: ua_node_get_browsename_const failed with error=%i\n", __func__, ret);
return ret;
}
for (i = 0; i < countof(g_sampleB_nodes); i++) {
const struct sampleB_node_info *node_info = &g_sampleB_nodes[i];
if (ua_string_compare_const(&bn.name, node_info->browsename) != 0) {
continue;
}
/* call our own callback from inside the callback */
if (node_info->action_cb != NULL) {
ret = node_info->action_cb(info->parent_node);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: node_info->action_cb failed with error=%i\n", __func__, ret);
}
}
/* the return value determines how the node is handled */
return node_info->node_action;
}
/* abort he instance creation if there is an unexpected 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:

static int create_color_node(ua_node_t parent)
{
struct ua_nodeid id;
ua_node_t color_node;
ua_nodeid_set_numeric(&id, g_nsidx, 3000);
UA_NODECLASS_VARIABLE,
g_nsidx,
"Color",
"Color",
ua_node_find0(UA_ID_BASEDATAVARIABLETYPE),
parent,
ua_node_find0(UA_ID_HASCOMPONENT));
if (color_node == UA_NODE_INVALID) return UA_EBAD;
return 0;
}

Watching the resulting instance with the UAExpert shows the EngineeringUnits are missing and <OtherFeature> is replaced with Color:

SampleB.png
SampleB

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:

SampleB_density.png
SampleB_density

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.

SampleC: TypeDefinitions and Sub-Instancing

This is an advanced technique, so a bit of background helps before showing the implementation of this sample.

Background

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 instance should have further optional nodes from the VariableType which are not present at the UA Variable of the type.
  • The UA Variable on the instance should use a different TypeDefinition than the one at the type with different child nodes at the VariableType (Note: to be a valid instance the new VariableType needs to be a subtype of the original VariableType).
  • The namespace with the types is provided by a third party and the child nodes are missing, so it's basically broken. The issue is reported but is probably never going to be fixed, nevertheless you still want to create correct instances.

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.

Implementation

This sample uses a simple type with three features and no Properties:

SampleCType.png
SampleCType

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:

struct sampleC_node_info {
const char *browsename;
uint32_t typedef_identifier;
};
static const struct sampleC_node_info g_sampleC_nodes[] = {
{"Volume", 0},
{"Weight", UA_ID_DATAITEMTYPE },
{"State", UA_ID_MULTISTATEDISCRETETYPE},
};

The TypeDefinition callback looks up the struct entry and searches for the namespace zero node given by their numeric identifier to return the result:

ua_node_t sampleC_typedef_cb(
struct ua_instance_ctx *ctx,
{
struct ua_qualifiedname bn;
unsigned int i;
int ret;
UA_UNUSED(ctx);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "%s: ua_node_get_browsename_const failed with error=%i\n", __func__, ret);
return ret;
}
for (i = 0; i < countof(g_sampleC_nodes); i++) {
const struct sampleC_node_info *node_info = &g_sampleC_nodes[i];
if (ua_string_compare_const(&bn.name, node_info->browsename) != 0) {
continue;
}
return ua_node_find0(node_info->typedef_identifier);
}
}

That has the following effects:

  • For Volume no node handle is found as 0 is an invalid namespace zero identifier, so UA_NODE_INVALID is returned which means to do no sub-instancing at all and the node at the instance looks exactly like at the type.
  • Weight returns the same TypeDefinition it already has, the sub-instancing is still started and creates all the (optional) Properties of DataItemType.
  • State returns MultiStateDiscreteType which is a subtype of DataItemType, so the sub-instancing creates all Properties of DataItemType and the additional Property EnumStrings from MultiStateDiscreteType.

Therefore the resulting instance looks like this:

SampleC.png
SampleC

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.

Conclusion

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:

  • If the type has any optional nodes, the pre create callback is very useful to determine which of these to create. That may not be as fine grained to handle each node individually but also more general rules like "copy all optional nodes" or "create a reference every method" are possible.
  • Most instances represent some dynamic values, so to connect these with the nodes the post create callback is a good opportunity. It is used only in one of the above samples because the other samples focus on different topics, but for meaningful instances these would need to connect their values as well.

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.