High Performance OPC UA Server SDK  1.2.0.193
Lesson 2: Custom Provider with Value Stores

This lesson explains how to create a custom provider to extend the address space and serve values using value stores.

Files used in this lesson:

Preliminary Note

The real world example used for this getting started lesson is an arbitrary device that contains three 32 bit unsigned integer variables. The variables will be represented by three UA nodes in the address space to allow read and write access to the device. As the device is not actually available, the variables are kept in memory for this example.

This lesson will introduce value stores, these are integrated in the SDK and offer easy access to values using different mechanisms. The implementation of a value store to access a device is much simpler than implementing service handlers for read, write, and subscription, but the value store is limited to devices with synchronous access to the data.

This lesson shows how to implement a custom value store to access the example device and how to use a value store for in-memory data implemented by the SDK.

Step 1: Create a Custom Provider

The new custom provider is implemented in custom_provider.c and needs an initialization function:

int custom_provider_init(struct uaprovider *ctx)

This function is added to the provider array in server_main.c.

static struct uaserver_provider g_provider[] = {
{-1, uaprovider_server_init},
{-1, custom_provider_init}
};

Register Address Space

Inside the init function the provider registers its address space. A new empty address space for the given URL is created, and the SDK assigned index of the namespace is returned and stored in a global variable. The ua_addressspace_register function takes a configuration structure with the maximum number of nodes and references that can be created inside the new namespace:

uint16_t g_custom_provider_nsidx;
ua_memset(&config, 0, sizeof(config));
config.nsidx = UA_NSIDX_INVALID; /* automatically assign nsindex */
config.max_variables = 10;
config.max_references = 20;
config.max_strings = 10;
/* register new namespace */
"http://www.unifiedautomation.com/CustomServer/",
&config,
if (ret < 0) return ret;
g_custom_provider_nsidx = (uint16_t) ret;

The new namespace index must be registered at the provider management:

/* register namespace index */
ret = uaprovider_register_nsindex(ctx, g_custom_provider_nsidx);
if (ret != 0) return ret;

Register Value Stores

The memory store is a value store for in-memory variants, implemented by the SDK. To create an instance of it, global variables are needed for the store context and the memory for the actual values.

static struct ua_variant g_memorystore_values[3];
struct ua_memorystore g_memorystore;

The store context must be initialized with the value array. When calling ua_memorystore_init the store is also registered at the global valuestore management with the storeindex given to the function. Here zero is used, which means a free index is automatically assigned.

/* initialize memorystore context */
ret = ua_memorystore_init(&g_memorystore, 0, g_memorystore_values, countof(g_memorystore_values));
if (ret != 0) return ret;

The custom store must be directly registered at the global valuestore management. The second parameter of ua_valuestore_register_store is an in-out parameter with the requested storeindex and is assigned the actual storeindex. Here again zero is used to automatically assign a free index.

/* register custom store */
store_if.get_fct = custom_store_get_value;
store_if.attach_fct = custom_store_attach_value;
ret = ua_valuestore_register_store(&store_if, &g_custom_store_idx);
if (ret != 0) return ret;

The implementation of the accessor functions passed to the registration will be shown in the next step.

Create Nodes

Now the nodes of the custom provider can be created. This happens in an own function custom_provider_create_nodes that will be covered later in this lesson:

/* create nodes in the namespace */
ret = custom_provider_create_nodes();
if (ret != 0) return ret;

Register Service Handlers

The service handler functions are registered by writing them into the provider context. These functions are called when the corresponding service request is received and the provider may need to take action. In this lesson, default implementations provided by the SDK can be used:

/* register service handler */
#ifdef ENABLE_SERVICE_READ
#endif
#ifdef ENABLE_SERVICE_WRITE
#endif
#ifdef ENABLE_SERVICE_REGISTERNODES
#endif
#ifdef ENABLE_SERVICE_SUBSCRIPTION
#endif

It is also possible to register a cleanup function that is called when the server shuts down. In this example it is used to unregister the value stores:

/* register cleanup function */
ctx->cleanup = custom_provider_cleanup;
/* cleanup resources allocated by the custom provider */
static void custom_provider_cleanup(struct uaprovider *ctx)
{
UA_UNUSED(ctx);
ua_memorystore_clear(&g_memorystore);
ua_valuestore_unregister_store(g_custom_store_idx);
return;
}

Step 2: Create a Custom Value Store

The value stores work with two indices: The store index is used to identify instances of value stores, the value index is used to identify a value inside a store and has a store specific meaning. Both indices are saved in variable nodes, and the SDK uses them find the associated store for the node and retrieve or write a value.

The custom store for the device is implemented in custom_provider_store.c and the functions are declared in custom_provider_store.h. The values of the device are directly saved in a global array of uint32_t, where the value index is used as array index. For a real device, this could also be an array of port numbers or device specific addresses:

static uint32_t g_custom_provider_values[3];
static struct custom_eu_range g_custom_provider_ranges[3];

There is a function to set the initial value for an index:

int custom_store_set_initial_value(unsigned int idx, uint32_t value) {
if (idx >= countof(g_custom_provider_values)) return -1;
/* write value to the array */
g_custom_provider_values[idx] = value;
return 0;
}

The other two functions are the accessor functions to get and set values, which are defined by the store.

Read Value

There is a getter function for reading a value from the store taking the following parameters:

store
Context of the store
node
Node to read value from
idx
The value index for the value to be read
source_ts
boolean to indicate whether the source timestamp should be set (Note: the server timestamp is set by the SDK)
range
Array with index ranges to be read
num_ranges
Length of the range array
result
pointer to preallocated ua_datavalue struct to write the result to

Implementing the function is simple: The value index must be checked to not read out of bounds of the array, and reading with index range is not possible as this is a scalar. Then the value and source timestamp are set (if required), and the status code is set to good. If an error happens during reading the value, the status code of the result is set to a bad status code and the function returns:

void custom_store_get_value(void *store, ua_node_t node, unsigned int idx, bool source_ts, struct ua_indexrange *range, unsigned int num_ranges, struct ua_datavalue *result)
{
UA_UNUSED(node);
UA_UNUSED(range);
UA_UNUSED(store);
/* check array bounds */
if (idx >= countof(g_custom_provider_values)) {
return;
}
/* there are only scalar values in the store */
if (num_ranges > 0) {
return;
}
/* write value to result */
ua_variant_set_uint32(&result->value, g_custom_provider_values[idx]);
/* add source timestamp if requested */
if (source_ts) ua_datetime_now(&result->source_timestamp);
/* set good statuscode */
result->status = 0;
}

Write Value

There is also a function for writing a value to the store. The parameters are similiar to the getter function, but instead of the result, the value to write is passed. Furthermore, in case of an error a bad status code must be passed back as return value.

The function itself again needs to check the value index and the index range. Then it has to make sure that only the value is written and no timestamp or status code. Finally the type of the value must be checked, and the value can be written to the corresponding slot of the array:

ua_statuscode custom_store_attach_value(void *store, ua_node_t node, unsigned int idx, struct ua_indexrange *range, unsigned int num_ranges, struct ua_datavalue *value)
{
uint32_t newval; /* value to write */
UA_UNUSED(range);
UA_UNUSED(store);
UA_UNUSED(node);
/* check array bounds */
if (idx >= countof(g_custom_provider_values)) return UA_SCBADINTERNALERROR;
/* there are only scalar values in the store */
if (num_ranges > 0) return UA_SCBADWRITENOTSUPPORTED;
/* write of status or timestamp is not supported */
if (value->status != 0 || value->server_timestamp != 0 || value->source_timestamp != 0) {
}
/* check type of value to write */
if (value->value.type != UA_VT_UINT32) {
}
newval = value->value.value.ui32;
/* check range */
if (newval > g_custom_provider_ranges[idx].eu_high ||
newval < g_custom_provider_ranges[idx].eu_low) {
}
/* write new value to the array */
g_custom_provider_values[idx] = newval;
return 0;
}

The function is called attach because it may also take the ownership of the value by creating a shallow copy and remove the value from the original ua_datavalue struct:

/* shallow copy */
struct ua_datavalue my_value = *value;
/* overwrite original value with zero */
/* clear my_value later */

Step 3: Create Nodes

The values from the device should be represented by variable nodes in the address space for being accessible for clients like UaExpert. Furthermore, each variable will get a property indicating its valid range. This happens in custom_provider_nodes.c

Create a Variable Node

To create multiple variable nodes, there is a function to create a single variable node that will be called multiple times. This function creates a new node from the node class variable using ua_node_create_with_attributes. The new node will get a type definition reference to basedatavariabletype and is referenced by its parent node with an organizes reference. Further attributes specific to the variable node are set with ua_variable_set_attributes.

ua_nodeid_set_numeric(&nodeid, g_custom_provider_nsidx, identifier);
/* create a node from class variable with mandatory attributes */
&nodeid, /* nodeid for the new node */
UA_NODECLASS_VARIABLE, /* nodeclass of the new node */
nodeid.nsindex, /* ns index for browsename is same as for nodeid */
name, /* browsename */
NULL, /* displayname, NULL for same as browsename */
UA_NODE_BASEDATAVARIABLETYPE, /* typedefinition is basedatavariabletype */
parent, /* parent node of the new node */
UA_NODE_ORGANIZES); /* new node is referenced with organizes by parent */
if (node == UA_NODE_INVALID) return UA_NODE_INVALID;
ua_nodeid_set_numeric(&datatype, 0, UA_ID_UINT32);
/* set mandatory attributes for nodeclass variable */
node, /* newly created node to set attributes to */
&datatype, /* datatype is uint32 */
UA_VALUERANK_SCALAR, /* valuerank: scalar */
UA_ACCESSLEVEL_CURRENTREAD | UA_ACCESSLEVEL_CURRENTWRITE, /* allow read and write for the value */
false); /* historizing is not supported for this node */
if (ret != 0) return UA_NODE_INVALID;
Note
Node handles for frequently used namespace zero nodes are provided by the SDK with variables like UA_NODE_ORGANIZES. Less commonly used node handles can be retrieved by calling the more general function ua_node_find0(UA_ID_ORGANIZES).

Then the initial value is set with the function implemented in Step 2: Create a Custom Value Store and the node is connected to the value by setting the store index and value index:

/* set initial value */
ret = custom_store_set_initial_value(valueidx, initial_value);
if (ret != 0) return UA_NODE_INVALID;
/* write the storeindex to the node */
ret = ua_variable_set_store_index(node, g_custom_store_idx);
if (ret != 0) return UA_NODE_INVALID;
/* write the valueindex to the node */
ret = ua_variable_set_value_index(node, valueidx);
if (ret != 0) return UA_NODE_INVALID;

Create a Property Node

Creating and adding a property works similar to creating the variable node in Create a Variable Node, because the property is also a variable node. So the same two functions can be used to create a new node and set the variable attributes. The main differences are the type definition and that the parent references this node with a hasproperty reference. Also note that the namespace index of the browse name is zero:

ua_nodeid_set_numeric(&nodeid, g_custom_provider_nsidx, identifier);
/* create a node from class variable with mandatory attributes */
&nodeid, /* nodeid for the new node */
UA_NODECLASS_VARIABLE, /* nodeclass of the new node */
0, /* ns index for browsename is namespace zero */
"EURange", /* browsename */
NULL, /* displayname, NULL for same as browsename */
UA_NODE_PROPERTYTYPE, /* typedefinition is propertytype */
parent, /* parent node of the new node */
UA_NODE_HASPROPERTY); /* new node is referenced with hasproperty by parent */
if (node == UA_NODE_INVALID) goto out;
ua_nodeid_set_numeric(&datatype, 0, UA_ID_RANGE);
/* set mandatory attributes for nodeclass variable */
if (ret != 0) goto out;

The property should contain a value from type range that indicates the minimum and maximum value the variable can have. However, this minimum is not enforced by the implementation and purely informative. First the range value must be allocated and the minimum/maximum values are set. Then the range is attached to a ua_variant and the ua_variant is attached to the memory store together with the new node:

/* allocate memory for ua_range struct */
eurange = IPC_ALLOC(eurange);
if (eurange == NULL) return UA_EBADNOMEM;
/* set minimum/maximum of the range */
eurange->low = low;
eurange->high = high;
/* attach range to an ua_variant */
ret = ua_variant_attach_extensionobject(&property, eurange, &datatype);
if (ret != 0) goto out;
/* attach the ua_variant to the memorystore */
ret = ua_memorystore_attach_new_value(&g_memorystore, &property, node);
if (ret != 0) goto out;

Instantiate New Nodes

Now a new folder for the custom nodes can be created by calling ua_object_create_folder and the new nodes are created using the functions implemented previously in Create a Variable Node and Create a Property Node

/* create nodes in the custom provider namespace */
int custom_provider_create_nodes(void)
{
ua_node_t folder, node;
struct ua_nodeid nodeid;
int ret;
/* create a folder for the new nodes */
ua_nodeid_set_numeric(&nodeid, g_custom_provider_nsidx, CUSTOM_NODES_ID);
folder = ua_object_create_folder(&nodeid, UA_NODE_OBJECTSFOLDER, "CustomNodes");
if (folder == UA_NODE_INVALID) return UA_EBAD;
/* create new variable node */
node = custom_provider_create_variable(CUSTOM_VARIABLE1_ID, folder, "Variable1", 0, 0);
if (node == UA_NODE_INVALID) return UA_EBAD;
/* create property eurange and add it to the new node */
ret = custom_provider_create_eurange(node, CUSTOM_EURANGE_VAR1_ID, 0.0, 50.0);
if (ret != 0) return ret;
node = custom_provider_create_variable(CUSTOM_VARIABLE2_ID, folder, "Variable2", 1, 5);
if (node == UA_NODE_INVALID) return UA_EBAD;
ret = custom_provider_create_eurange(node, CUSTOM_EURANG_VAR2_ID, 0.0, 100.0);
if (ret != 0) return ret;
node = custom_provider_create_variable(CUSTOM_VARIABLE3_ID, folder, "Variable3", 2, 93738);
if (node == UA_NODE_INVALID) return UA_EBAD;
ret = custom_provider_create_eurange(node, CUSTOM_VARIABLE3_ID, 0.0, 100000.0);
if (ret != 0) return ret;
return 0;
}

Step 4: Run Application

Compile and run the server application. When connecting to the server with a UA Client (e.g. UaExpert) the newly created nodes are visible in the server’s address space. It is also possible to read and subscribe to values and write to the variables.