High Performance OPC UA Server SDK  1.7.1.383
Lesson 3: Creating Methods

This lesson explains how to implement UA methods.

Files used in this lesson:

Preliminary Note

The server from the previous lesson is extended by a node called “Object1” containing a method named “MultiplyMethod”. The method is rather simple and multiplies two given floats to return the result as float. This lesson will show how to create the necessary nodes in the address space and to implement the method in the server.

In this lesson all the steps are done manually by implementing in code. For larger projects this is hardly feasible, so engineering tools like the UA Modeler generate an entire name space including the method nodes which can be loaded into the server. The UA Modeler can also generate the code for the argument handling, so in the best case all that's left from this lesson is the actual method implementation and registration.

Step 1: Create memorystore for method arguments

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[10];
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 store is needed to store the method arguments.

Step 2: Create Nodes Necessary for multiply Method

The file custom_provider_nodes.c from the previous lesson is extended to create the required nodes.

First the actual method node of nodeclass method is created. This node represents the method in the address space and is used as MethodId in the call service. It is created as child of “Object1”, which must be used as ObjectId in the call service when calling this method.

Furthermore the method node requires two two child nodes for describing the input and output arguments of the method. These two nodes are UA Properties, so they are of class variable and their browse name is crucial. Methods that have no input or output arguments also don't need the respective input/output argument node.

Create Method Node

ua_nodeid_set_numeric(&nodeid, g_custom_provider_nsidx, identifier);
&nodeid, /* nodeid for the new node */
UA_NODECLASS_METHOD, /* nodeclass of the new node */
nodeid.nsindex, /* ns index for browsename is same as nodeid */
method_name, /* browsename */
NULL, /* displayname, NULL for same as browsename */
UA_NODE_INVALID, /* the node has no typedefinition */
parent, /* parent node of the new node */
UA_NODE_HASCOMPONENT); /* new node is referenced with hascomponent by parent */
if (method_node == UA_NODE_INVALID) return -1;
ret = ua_method_set_executable(method_node, true);
if (ret != 0) return ret;

Create Input Argument Nodes

/* input nodes */
ua_nodeid_set_numeric(&nodeid, g_custom_provider_nsidx, MULTIPLY_INPUT_ARGUMENTS_ID);
input_argument_node = ua_node_create_with_attributes(
&nodeid, /* nodeid for the new node */
UA_NODECLASS_VARIABLE, /* nodeclass of properties is variable */
0, /* ns index for browsename is namespace zero */
"InputArguments", /* browsename */
NULL, /* displayname, NULL is same as browsename */
UA_NODE_PROPERTYTYPE, /* the node has the typedefinition propertytype */
method_node, /* parent of the new node is the method node */
UA_NODE_HASPROPERTY); /* new node is referenced with hasproperty by parent */
if (input_argument_node == UA_NODE_INVALID) return -1;
ua_nodeid_set_numeric(&datatype_id, 0, UA_ID_ARGUMENT);
ret = ua_variable_set_attributes(input_argument_node, &datatype_id, UA_VALUERANK_ONEDIMENSION, UA_ACCESSLEVEL_CURRENTREAD, false);
if (ret != 0) return ret;

Create Output Argument Nodes

/* output node */
ua_nodeid_set_numeric(&nodeid, g_custom_provider_nsidx, MULTIPLY_OUTPUT_ARGUMENTS_ID);
output_argument_node = ua_node_create_with_attributes(
&nodeid, /* nodeid for the new node */
UA_NODECLASS_VARIABLE, /* nodeclass of properties is variable */
0, /* ns index for browsename is namespace zero */
"OutputArguments", /* browsename */
NULL, /* displayname, NULL is same as browsename */
UA_NODE_PROPERTYTYPE, /* the node has the typedefinition propertytype */
method_node, /* parent of the new node is the method node */
UA_NODE_HASPROPERTY); /* new node is referenced with hasproperty by parent */
if (output_argument_node == UA_NODE_INVALID) return -1;
ua_nodeid_set_numeric(&datatype_id, 0, UA_ID_ARGUMENT);
ret = ua_variable_set_attributes(output_argument_node, &datatype_id, UA_VALUERANK_ONEDIMENSION, UA_ACCESSLEVEL_CURRENTREAD, false);
if (ret != 0) return ret;

Step 3: Initialize Method Arguments

In this step the values of the input and output argument properties are set. Both are of type argument, which describes the arguments so universal clients can for example present a GUI dialog and issue a call request with the correct datatypes of the arguments. The value is always an array with an entry for each argument, so in this lesson the input argument has length two and the output argument length one. Each argument is described by a name and description, which can be freely chosen and the datatype and valuerank, which determines the type and rank of values the client needs to send when invoking the call service.

The values of the input and output arguments are persisted in the memorystore for this lesson.

Initialize Input Arguments

/* find input arguments node */
input_argument_node = ua_node_find_numeric(g_custom_provider_nsidx, MULTIPLY_INPUT_ARGUMENTS_ID);
if (input_argument_node == UA_NODE_INVALID) return -1;
/* create value of input arguments */
ua_nodeid_set_numeric(&type_id, 0, UA_ID_ARGUMENT);
ua_argument_init(&args[0]);
ua_string_attach_const(&args[0].name, "a");
ua_string_attach_const(&args[0].description.text, "factor a");
ua_string_attach_const(&args[0].description.locale, "en-US");
ua_nodeid_set_numeric(&args[0].data_type, 0, UA_VT_FLOAT);
args[0].value_rank = UA_VALUERANK_SCALAR;
ua_argument_init(&args[1]);
ua_string_attach_const(&args[1].name, "b");
ua_string_attach_const(&args[1].description.text, "factor b");
ua_string_attach_const(&args[1].description.locale, "en-US");
ua_nodeid_set_numeric(&args[1].data_type, 0, UA_VT_FLOAT);
args[1].value_rank = UA_VALUERANK_SCALAR;
ret = ua_variant_set_extensionobject_array(&v, args, 2, &type_id);
if (ret != 0)
return -1;
/* attach the value to the node */
ret = ua_memorystore_attach_new_value(&g_memorystore, &v, input_argument_node);
if (ret != 0) {
return -1;
}

Initialize Output Arguments

/* find output arguments node */
output_argument_node = ua_node_find_numeric(g_custom_provider_nsidx, MULTIPLY_OUTPUT_ARGUMENTS_ID);
if (output_argument_node == UA_NODE_INVALID) return -1;
/* create value of output arguments */
ua_argument_init(&args[0]);
ua_string_attach_const(&args[0].name, "product");
ua_string_attach_const(&args[0].description.text, "product = a * b");
ua_string_attach_const(&args[0].description.locale, "en-US");
ua_nodeid_set_numeric(&args[0].data_type, 0, UA_VT_FLOAT);
args[0].value_rank = UA_VALUERANK_SCALAR;
ret = ua_variant_set_extensionobject_array(&v, args, 1, &type_id);
if (ret != 0)
return -1;
/* attach the value to the node */
ret = ua_memorystore_attach_new_value(&g_memorystore, &v, output_argument_node);
if (ret != 0) {
return -1;
}

Step 4: Add argument handler

The following steps are implemented in custom_provider_method.c and demonstrate the implementation of the method.

This step shows the argument handler function, which checks the arguments provided by the client for their number and type, calls the function implementing the actual method functionality and attaches the output arguments to the response sent back to the client.

Check Input Arguments

The argument checks are rather complex as the OPC UA Specification has precise requirements for the statuscodes returned in case these don't match, so the SDK provides a utility function for this job: uaserver_call_utility_check_arguments. It takes an array of type descriptions of each input argument which contains the a pointer to the namespace index in case of a complex type the type ID, the variant type, and flags to influence the behaviour of the function. This lesson only uses floats which are in namespace zero, so one array entry looks like this:

{&g_uaprovider_server_nsidx, 0, UA_VT_FLOAT, UASERVER_CALL_UTILITY_FLAG_NONE}

If an argument is an array of uint32 it would look like this:

{&g_uaprovider_server_nsidx, 0, UA_VT_UINT32 | UA_VT_IS_ARRAY, UASERVER_CALL_UTILITY_FLAG_NONE}

For the complex type EUInformation it would look like this:

{&g_uaprovider_server_nsidx, UA_ID_EUINFORMATION, UA_VT_EXTENSIONOBJECT, UASERVER_CALL_UTILITY_FLAG_NONE}

The utility function takes this array with type information and checks each argument in the request for the correct type, in case of a mismatch it fills the correct statuscodes in the response and returns a bad statuscode.

Call the Method Implementation

When calling the actual method implementation the respective fields of the variant are directly accessed and passed over, this is safe as the types are already checked. The output arguments are passed as pointers, so the callee can write the result directly into the stack variables. Arrays would be passed as double pointers and with a pointer to the length field, so the callee can allocate the suitable length and write the length field.

Attach Output Arguments to the Response

The output arguments need to be put into a variant array in the response, the SDK provides another utility: uaserver_call_utility_attach_arguments. This function takes an array of datatype descriptions just like in Initialize Input Arguments, additionally the output arguments are passed in the same order as in the datatype array. The function takes a variable number of arguments so it works for all methods even though they have a different number of output arguments.

For further usage details and an example for different datatypes see the documentation of that function: uaserver_call_utility_attach_arguments

Conclusion

This is the complete argument handler function including the input/output argument description:

/* description of the input arguments */
static const struct uaserver_call_utility_arg g_multiply_in_args[] = {
{&g_uaprovider_server_nsidx, 0, UA_VT_FLOAT, UASERVER_CALL_UTILITY_FLAG_NONE},
{&g_uaprovider_server_nsidx, 0, UA_VT_FLOAT, UASERVER_CALL_UTILITY_FLAG_NONE},
};
/* description of the output arguments */
static const struct uaserver_call_utility_arg g_multiply_out_args[] = {
{&g_uaprovider_server_nsidx, 0, UA_VT_FLOAT, UASERVER_CALL_UTILITY_FLAG_NONE},
};
static ua_statuscode custom_provider_call_multiply(
struct uaprovider_call_ctx *ctx,
const struct ua_callmethodrequest *req,
struct ua_callmethodresult *res)
{
float out_product = 0;
/* check argument types */
status_code = uaserver_call_utility_check_arguments(g_multiply_in_args, countof(g_multiply_in_args), req, res);
if (status_code != 0) return status_code;
/* call the actual method implementation */
status_code = custom_provider_method_multiply( //-V1048
ctx,
&req->method_id,
&req->object_id,
req->input_arguments[0].value.f,
req->input_arguments[1].value.f,
&out_product
);
if (!ua_statuscode_is_good(status_code)) {
goto error;
}
/* attach the output arguments to the response */
res,
g_multiply_out_args,
countof(g_multiply_out_args),
&out_product
);
if (status_code != 0) {
goto error;
}
/* success */
return 0;
error:
return status_code;
}

This section presented a universal recipe to implement the argument handling function correctly also for complex situations. The UA Modeler also generates these functions in the same manner, which is the prefered way over implementing these manually. However it is still possible or may even be necessary to implement this function in a completely different way to meet special requirements as long as the signature of the function is preserved.

Step 5: Method Implementation

The implementation of the actual method implementation is rather simple for this lesson, there is nothing more than multiplying the inputs, assigning the result to the output argument and returning a good statuscode:

static ua_statuscode custom_provider_method_multiply(
/* in */ struct uaprovider_call_ctx *ctx,
/* in */ const struct ua_nodeid *method_id,
/* in */ const struct ua_nodeid *object_id,
/* in */ float a,
/* in */ float b,
/* out */ float *product)
{
UA_UNUSED(ctx);
UA_UNUSED(method_id);
UA_UNUSED(object_id);
*product = a * b;
return 0;
}

There are a few further arguments, which are not used in this example but may come in handy in other situations. The uaprovider_call_ctx contains metadata like the uasession_session that may be used for further access checks. The ObjectId is not used as this method is side effect free, but many methods can alter the state of the object they are called on. The MethodId could be used to register the same handler function for different methods (with the same input and output arguments), but this is rarely the case.

Step 6: Register the Handler Function

As of version 1.4.0 of the SDK there is an improved way of finding the correct method handler function for a given ObjectId/MethodId combination. In previous version it was required to implement an own service handler function, this is still possible, but now the SDK has its own default service handler. This service handler uses a global table, that allows the registration of method handlers for certain ObjectId/MethodId combinations.

For this lesson registration is simple as the argument handling function is registered as handler for the exact combination of “Object1” and the new multiply method:

int custom_provider_method_init(void)
{
ua_node_t method_node, object_node;
/* find the object node */
object_node = ua_node_find_numeric(g_custom_provider_nsidx, DEMO_OBJECT_ID);
if (object_node == UA_NODE_INVALID) return UA_EBADNOTFOUND;
/* find the method node */
method_node = ua_node_find_numeric(g_custom_provider_nsidx, MULTIPLY_METHOD_ID);
if (method_node == UA_NODE_INVALID) return UA_EBADNOTFOUND;
return uaserver_call_table_register(object_node, method_node, custom_provider_call_multiply);
}

ObjectId

A method handler is always registered for a specific method node, but depending on the node given as ObjectId a method handler can be registered for different scopes regarding the object:

Scope ObjectId Description
Object Object The handler is registered only for that exact object like in the example
Type ObjectType The handler is registered for all objects of that type (also including subtypes)
Global UA_NODE_INVALID The handler is registered for every object (with the matching MethodId)

The most common case should be the type scope as object of the same type usually share the same implementation. It is however possible to have different handlers registered for the same MethodId in different scopes at the same time. So handler A could be registered for the ObjectType, but a single object might need a different handling, so handler B is registered for that object only. Addtionally as fallback for objects of different types the global handler C is registered.

MethodId

Version 1.4.0 of the SDK also brings support for the MethodDeclarationId. This is an additional information for methods on an object and points to the method on the object type of the object (also known as InstanceDeclaration). So it is basically a reference to the original method, set of course only if such a node exists. The MethodDeclarationId is part of the XML schema for UA address spaces and present in most XML files from decent engineering tools. It is also set when creating an instance at runtime using the instance functionality.

Regarding this lesson it is important to know the default call service handler will follow the MethodDeclarationId and perform the lookup in the call table based on that node. The consequence is that the lookup will not work when using a method node with a set MethodDeclarationId for registrations. So in most case the method node from the tpye must be used for registration, the only exceptions are when the method it not from a type (like in this lesson) or the address space is from a source that does not support the MethodDeclarationId.

Attention
Always register the method node from the type (InstanceDeclaration) if possible

Step 7: Call the Method with UA Expert

Implementation is finished, it is time to call the method from a client. Start the server and use UA Expert to connect, in the address space window navigate to Root->Objects->CustomNodes->Object1->MultiplyMethod. Use a right click and select "Call..." to open the call window. Here enter some numbers and click "Call" to let the server do the multiplication.