High Performance OPC UA Server SDK  1.6.1.357
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;
Implementation of a store using in-memory values.
Definition: memorystore.h:51
Structure for an UA Variant, see also ua_variant.
Definition: variant_types.h:141

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;
#define countof(arr)
Returns the size of an array (number of elements).
Definition: pplatform.h:146
int ua_memorystore_init(struct ua_memorystore *store, uint8_t storeidx, struct ua_variant *values, unsigned int max_values)
Initialize a memorystore with a preallocated ua_variant array.
Definition: memorystore.c:255

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;
ua_node_t ua_node_create_with_attributes(struct ua_nodeid *nodeid, enum ua_nodeclass nodeclass, uint16_t browsename_nsidx, const char *browsename, const char *displayname, ua_node_t typedef_node, ua_node_t parent, ua_node_t reftype_node)
Create a new node with basic attributes.
Definition: node.c:239
#define UA_NODE_INVALID
Value of an invalid node handle.
Definition: node.h:59
int ua_method_set_executable(ua_node_t node, bool executable)
Sets the executable bit on the given method.
Definition: method.c:139
void ua_nodeid_set_numeric(struct ua_nodeid *id, uint16_t nsindex, uint32_t value)
Set a numeric value and namespace index for a nodeid.
Definition: nodeid.c:85

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;
#define UA_ACCESSLEVEL_CURRENTREAD
The current value of the Variable may be read.
Definition: accesslevel.h:49
#define UA_VALUERANK_ONEDIMENSION
The variable is always a one dimensional array.
Definition: valuerank.h:57
int ua_variable_set_attributes(ua_node_t node, struct ua_nodeid *datatype_id, int32_t valuerank, uint8_t access_level, bool historizing)
Set the mandatory attributes of a variable node.
Definition: variable.c:578

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;
}
ua_node_t ua_node_find_numeric(uint16_t nsindex, uint32_t numeric_id)
Convenience function for finding nodes based on their numeric nodeid.
Definition: node.c:1470
#define UA_VALUERANK_SCALAR
The variable is always a scalar.
Definition: valuerank.h:51
int ua_variant_set_extensionobject_array(struct ua_variant *v, const void *obj, size_t num, const struct ua_nodeid *type_id)
Stores an array of custom structures in a variant.
Definition: variant_setter.c:709
void ua_variant_clear(struct ua_variant *v)
clear an ua_variant.
Definition: variant.c:135
void ua_argument_init(struct ua_argument *t)
Initialize an ua_argument struct with a valid value.
Definition: argument.c:75
int ua_memorystore_attach_new_value(struct ua_memorystore *store, struct ua_variant *value, ua_node_t node)
Attach a new value to the memorystore.
Definition: memorystore.c:128
void ua_string_attach_const(struct ua_string *s, const char *src)
Initializes s with string constant src.
Definition: string.c:295

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}
@ UASERVER_CALL_UTILITY_FLAG_NONE
No flag is given, use the default behaviour.
Definition: call_utility.h:49

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;
}
uint32_t ua_statuscode
A numerical value indicating outcome of an UA server operation, possible values are listed in ua_stat...
Definition: statuscode.h:89
static bool ua_statuscode_is_good(ua_statuscode status)
Test if a statuscode is good.
Definition: statuscode.h:127
#define UA_SCBADOUTOFMEMORY
Not enough memory to complete the operation.
Definition: statuscodes.h:158
ua_statuscode uaserver_call_utility_attach_arguments(struct ua_callmethodresult *res, const struct uaserver_call_utility_arg *types, int32_t num_types,...)
Attach output arguments to the ua_callmethodresult.
Definition: call_utility.c:231
ua_statuscode uaserver_call_utility_check_arguments(const struct uaserver_call_utility_arg *types, int32_t num_types, const struct ua_callmethodrequest *req, struct ua_callmethodresult *res)
Utility function to check the input arguments provided by a client for a method.
Definition: call_utility.c:77
A structure that is defined as the type of the methodsToCall parameter of the Call service.
Definition: callmethodrequest.h:92
struct ua_nodeid object_id
The NodeId shall be that of the object or ObjectType that is the source of a HasComponent Reference (...
Definition: callmethodrequest.h:93
struct ua_nodeid method_id
NodeId of the method to invoke.
Definition: callmethodrequest.h:94
struct ua_variant * input_arguments
List of input argument values.
Definition: callmethodrequest.h:95
A structure that is defined as the type of the results parameter of the Call service.
Definition: callmethodresult.h:101
Context given to the call service handler.
Definition: provider.h:304
Structure to describe a method input or output argument.
Definition: call_utility.h:58

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;
}
#define UA_UNUSED(x)
Indicates to the compiler that the parameter with the specified name is not used in the body of a fun...
Definition: pplatform.h:71
Structure for an UA Nodeid, see also ua_nodeid.
Definition: nodeid.h:175

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);
}
int32_t ua_node_t
Handle for a node in the addressspace.
Definition: node.h:57
int uaserver_call_table_register(ua_node_t object, ua_node_t method, uaserver_call_table_method_t fct)
Register a method function pointer for a certain object/method combination.
Definition: call_table.c:150

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.