High Performance OPC UA Server SDK  1.3.0.231
Lesson 1: Setting up an OPC UA Client Console Application

This lesson will guide you through the process of setting up a basic OPC UA Client console application. The application will connect to a server, read the values of two variable nodes, browse two nodes for references and disconnect.

Files used in this lesson:

Prerequisites

The recommended way of building applications with the SDK is using CMake. This and the further lessons rely on CMake being installed on the build machine.

Step 1: CMakeLists.txt

The CMakeLists.txt is similar to the server example, but instead of the SDK_SERVER_LIBRARIES the SDK_CLIENT_LIBRARIES are needed to link a client application. In case of linking a combined client and server application, both libraries would be required.

project(client_lesson01 C)
cmake_minimum_required(VERSION 3.0)
# include sdk.cmake delivered with the SDK
include(../../../sdk.cmake)
include(InstallIfNewer)
# create list of source files
set(SOURCES main.c sampleclient.c)
add_executable(${PROJECT_NAME} ${SOURCES} ${SDK_BSP_SRCS} ${SDK_ASM_SRCS})
# specify libraries to link
target_link_libraries(${PROJECT_NAME}
${SDK_CLIENT_LIBRARIES}
${SDK_BASE_LIBRARIES}
${OS_LIBS}
${SDK_SYSTEM_LIBS}
)
install(TARGETS ${PROJECT_NAME} DESTINATION bin)
# MS Visual Studio settings
include(VSSetDebugConfig)
vs_set_debug_config(TARGET ${PROJECT_NAME}
INSTALL_DIR_EXECUTABLE
WORKING_DIR "${CMAKE_INSTALL_PREFIX}/bin")
set_target_properties(${PROJECT_NAME} PROPERTIES FOLDER Applications/Examples/GettingStarted)

Step 2: Sample Client

The Sample Client consists of the public header sampleclient.h and the implementation in sampleclient.c.

There is a struct to hold the context information of the Sample Client:

#include <uaclient/uaclient.h>
struct sample_client {
struct ua_client client; /* Client context from the SDK */
enum sample_client_states cur_state; /* Current state of the Sample Client */
const char *url; /* Endpoint url to connect to */
int result; /* Result of the Sample Client */
struct uaapplication *app; /* Application object */
};

It is possible to create multiple instance of the Sample Client in one application by allocating multiple of these structs. To keep it simple this example only creates only one instance.

There are also functions to operate on this struct:

int sample_client_init(struct sample_client *ctx, const char *url);
void sample_client_cleanup(struct sample_client *ctx);
bool sample_client_check_state(struct sample_client *ctx);
int sample_client_get_result(struct sample_client *ctx);

The implementation of these file will be explained later on.

State Machine

The SDK provided client functions are asynchronous, so for a clear implementation the Sample Client has its own simple state machine. The states are defined by an enum:

enum sample_client_states {
SAMPLE_CLIENT_STATE_INITIAL, /* Start state, nothing has been done yet */
SAMPLE_CLIENT_STATE_CONNECTING, /* Client is currently trying to connect */
SAMPLE_CLIENT_STATE_CONNECTED, /* Connecting is finished */
SAMPLE_CLIENT_STATE_READING, /* Client is currently reading */
SAMPLE_CLIENT_STATE_READ_DONE, /* Reading is finished */
SAMPLE_CLIENT_STATE_BROWSING, /* Client is currently browsing */
SAMPLE_CLIENT_STATE_BROWSE_DONE, /* Browsing is finished */
SAMPLE_CLIENT_STATE_DISCONNECT, /* Client should disconnect */
SAMPLE_CLIENT_STATE_DISCONNECTING, /* Client is currently disconnecting */
SAMPLE_CLIENT_STATE_FINISHED /* All operations are finished */
};

The states are processed one after each other in the same order as they are defined. Only in case of error some states are skipped and the state machine jumps directly to DISCONNECT or FINISHED depending on wether the client is already connected when the error occurs. To make sure actions are triggered sample_client_check_state() must be called regularly:

bool sample_client_check_state(struct sample_client *ctx)
{
switch (ctx->cur_state) {
case SAMPLE_CLIENT_STATE_INITIAL:
sample_client_connect(ctx);
break;
case SAMPLE_CLIENT_STATE_CONNECTED:
sample_client_read(ctx);
break;
case SAMPLE_CLIENT_STATE_READ_DONE:
sample_client_browse(ctx);
break;
case SAMPLE_CLIENT_STATE_BROWSE_DONE:
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECT;
break;
case SAMPLE_CLIENT_STATE_DISCONNECT:
sample_client_disconnect(ctx);
break;
case SAMPLE_CLIENT_STATE_FINISHED:
return false;
default:
break;;
}
return true;
}

This function returns true as long as there is still work to do and false to indicate it has finished.

Connect

Before starting the connect the Sample Client is initialized, here the SDK client member is initialized and callbacks are registered. For registering callbacks the struct is first completely set to zero and then the implemented function pointers are set. This is recommended as further callbacks might be added in future versions and this allows you code to stay compatible. Also the Sample Client context is set as userdata, so the context can be accessed with ua_client_get_userdata() from any client callback.

int sample_client_init(struct sample_client *ctx, const char *url)
{
struct ua_client_callbacks callbacks;
int ret;
ret = ua_client_init(&ctx->client, ctx->app);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: ua_client_init failed with errorcode=%i\n", __func__, ret);
return ret;
}
callbacks.state_changed_cb = sample_client_connection_status_changed_cb;
/* TODO: improve or remove this callback? */
callbacks.error_cb = sample_client_connection_error_cb;
ua_client_set_callbacks(&ctx->client, &callbacks);
ua_client_set_userdata(&ctx->client, ctx);
ctx->cur_state = SAMPLE_CLIENT_STATE_INITIAL;
ctx->url = url;
ctx->result = 0;
return 0;
}

The registered callback function is called every time the connection status of the client changes. It is used for pure information here:

static void sample_client_connection_status_changed_cb(struct ua_client *client, enum ua_client_connection_state status)
{
const char *status_str = ua_client_connection_state_to_string(status);
UA_UNUSED(client);
printf("Connection Status Changed: %s\n", status_str);
}

After initializing the client now the connect operation can be started. All operations in the client SDK that involve network communication with the server are asynchronous. These functions can be easily recognized by the word 'begin' in their function name, also they have usually a callback function and callback data as parameters. It is important to check their return value, as in case of error the callback function is not called and the caller is responsible to free any memory passed to the function. In case of success the callback function is always called.

So starting the connect operation is rather simple, ua_client_begin_connect() is called and the synchronous result is checked to set the next state of the Sample Client:

static void sample_client_connect(struct sample_client *ctx)
{
int ret;
ret = ua_client_begin_connect(&ctx->client, ctx->url, sample_client_connect_cb, ctx);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: ua_client_begin_connect failed with errorcode=%i\n", __func__, ret);
ctx->result = ret;
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
} else {
ctx->cur_state = SAMPLE_CLIENT_STATE_CONNECTING;
}
}

The associated callback function gets as first argument the client context used for the begin call, ua_client_get_userdata() could be used to get the Sample Client context, but in this case the context was given as the callback data, so it is enough to cast the second argument to the correct pointer type.

The result of the operation is passed in the last two arguments. There is an int that has one of the values from <common/errors.h> to indicate an error in the client SDK, e.g. out of memory or a reponse could not be decoded. This is the first result to check, when it is zero (good) then the ua_statuscode argument must be checked. It contains the result that was generated and sent by the server in its response. In the example both results are checked and the state is set accordingly:

static void sample_client_connect_cb(struct ua_client *client, void *cb_data, int result, ua_statuscode statuscode)
{
struct sample_client *ctx = cb_data;
UA_UNUSED(client);
if (result != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: failed with errorcode=%i\n", __func__, result);
ctx->result = result;
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
} else if (statuscode != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: failed with statuscode=0x%08X\n", __func__, statuscode);
ctx->result = UA_EBADSERVICERESPONSE;
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
} else {
TRACE_INFO(TRACE_FAC_USERAPPLICATION, "%s: successfully connected\n", __func__);
ctx->cur_state = SAMPLE_CLIENT_STATE_CONNECTED;
}
}

Read

Next the Sample Client will read the values of two variable nodes from the server using the OPC UA read service. For most services there are functions available in <uaclient/uaclient_services.h> which work all very similar, except for the type of the request parameter of course. So for the read service ua_client_begin_read() is used.

The ua_readrequest struct can be simply put on the stack, but it is important the members are allocated with ipc_malloc (or a function that uses ipc_malloc). On success ua_client_begin_read() will make a shallow copy of the request and so take ownership of the members.

static void sample_client_read(struct sample_client *ctx)
{
struct ua_readrequest req;
int ret;
IPC_CLEAR(&req);
if (ret != 0) goto error;
req.nodes[0].attribute_id = UA_ATTRIBUTEID_VALUE;
ua_nodeid_set_numeric(&req.nodes[0].node_id, 0, UA_ID_SERVER_SERVERSTATUS_STATE);
req.nodes[1].attribute_id = UA_ATTRIBUTEID_VALUE;
ua_nodeid_set_numeric(&req.nodes[1].node_id, 0, UA_ID_SERVER_SERVERSTATUS_CURRENTTIME);
req.max_age = 0.0;
&ctx->client, /* client context */
NULL, /* service settings, optional */
&req, /* request */
sample_client_read_cb, /* callback function */
ctx); /* callback data */
if (ret != 0) goto error;
IPC_CLEAR(&req); /* client SDK is now responsible for this memory */
ctx->cur_state = SAMPLE_CLIENT_STATE_READING;
return;
error:
ctx->result = ret;
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECT;
return;
}

The callback function receives the the client result which must be checked first, only if it is good, the response header is guaranteed to be present, else it might be NULL. Then again only if the service result inside the response header is good, the response is guaranteed to be present. The parameters of the callback functions are the same for all services, except for the type of the response.

After the checks the Sample Client prints all received values. The content of the response is freed by the client SDK after the callback, but for a more sophisticated application that still needs to use parts of the response it is possible to detach values, and set the original values to zero.

The callback also contains the ua_readrequest as parameter, please note that it is only set when the service setting keep_request is set to true. In this case the request can be accessed in if its information is still useful or should be detached.

static int sample_client_read_cb(
struct ua_client *client,
int client_result,
struct ua_responseheader *rh,
struct ua_readresponse *res,
struct ua_readrequest *req,
void *cb_data)
{
struct sample_client *ctx = cb_data;
struct ua_string result_string;
struct ua_datavalue *result;
int ret;
int32_t i;
UA_UNUSED(client);
UA_UNUSED(req);
if (client_result != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: service failed with errorcode=%i\n", __func__, client_result);
ctx->result = client_result;
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECT;
return 0;
} else if (rh->service_result != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: service failed with statuscode=0x%08X\n", __func__, rh->service_result);
ctx->result = UA_EBADSERVICERESPONSE;
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECT;
return 0;
} else if (res->num_results != 2) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: expected 2 results, but received %u\n", __func__, res->num_results);
ctx->result = UA_EBADINTERNALERROR;
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECT;
return 0;
}
for (i = 0; i < res->num_results; i++) {
result = &res->results[i];
printf("Read result [%i]:\n", i);
if (ua_statuscode_is_bad(result->status)) {
printf(" Bad statuscode=0x%08X\n", result->status);
continue;
}
#ifdef ENABLE_TO_STRING
ret = ua_variant_to_string(&result->value, &result_string);
#else
ret = -1;
#endif
if (ret != 0) {
printf(" Could not be converted with 'ua_variant_to_string'\n");
continue;
}
printf(" Value=%s\n", ua_string_const_data(&result_string));
ua_string_clear(&result_string);
}
ctx->cur_state = SAMPLE_CLIENT_STATE_READ_DONE;
return 0;
}

Browse

The browse service is also available from <uaclient/uaclient_services.h> via ua_client_begin_browse() and ua_client_begin_browsenext(). However using these to functions to handle continuation points properly is rather difficult. For this reason this client SDK offers an easier way to browse: ua_client_begin_simple_browse() from <uaclient/uaclient_simple_browse.h>. This function handles continuation points and various recoverable errors in a transparent way. It can be called with the complete array of nodes to browse and returns a response with the complete results in its callback function.

The code to start the browse operation is similar to the read above:

static void sample_client_browse(struct sample_client *ctx)
{
struct ua_browserequest req;
int ret;
IPC_CLEAR(&req);
if (ret != 0) goto error;
req.nodes[0].browse_direction = UA_BROWSEDIRECTION_FORWARD;
req.nodes[0].include_subtypes = true;
req.nodes[0].node_class_mask = UA_NODECLASS_UNSPECIFIED; /* all nodeclasses */
req.nodes[0].result_mask = UA_BROWSERESULTMASK_ISFORWARD
| UA_BROWSERESULTMASK_DISPLAYNAME
| UA_BROWSERESULTMASK_NODECLASS;
ua_nodeid_set_numeric(&req.nodes[0].reference_type_id, 0, UA_ID_HIERARCHICALREFERENCES);
ua_nodeid_set_numeric(&req.nodes[0].node_id, 0, UA_ID_OBJECTSFOLDER);
req.nodes[1].browse_direction = UA_BROWSEDIRECTION_FORWARD;
req.nodes[1].include_subtypes = true;
req.nodes[1].node_class_mask = UA_NODECLASS_UNSPECIFIED; /* all nodeclasses */
req.nodes[1].result_mask = UA_BROWSERESULTMASK_ISFORWARD
| UA_BROWSERESULTMASK_DISPLAYNAME
| UA_BROWSERESULTMASK_NODECLASS;
ua_nodeid_set_numeric(&req.nodes[1].reference_type_id, 0, UA_ID_HIERARCHICALREFERENCES);
ua_nodeid_set_numeric(&req.nodes[1].node_id, 0, UA_ID_SERVER);
ret = ua_client_begin_simple_browse(&ctx->client, /* client context */
NULL, /* simple browse settings, optional */
&req, /* browserequest */
sample_client_browse_cb, /* callback function */
ctx); /* callback data */
if (ret != 0) goto error;
IPC_CLEAR(&req);
ctx->cur_state = SAMPLE_CLIENT_STATE_BROWSING;
return;
error:
ctx->result = ret;
ctx->cur_state = SAMPLE_CLIENT_STATE_BROWSE_DONE;
return;
}

The callback function is also similar to the above. There are two differences: First the result can be a combination of multiple service calls, so the there is no response header, but only the servcie result. Second the request is always set in the callback, this can be used to detach values that were allocated for the ua_client_begin_simple_browse() call.

The Sample Client again prints all results:

static int sample_client_browse_cb(
struct ua_client *client,
int client_result,
ua_statuscode service_result,
struct ua_browseresponse *res,
struct ua_browserequest *req,
void *cb_data)
{
struct sample_client *ctx = cb_data;
int32_t i,j;
UA_UNUSED(client);
if (client_result != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: service failed with errorcode=%i\n", __func__, client_result);
ctx->result = client_result;
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECT;
return 0;
} else if (service_result != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: service failed with statuscode=0x%08X\n", __func__, service_result);
ctx->result = UA_EBADSERVICERESPONSE;
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECT;
return 0;
}
for (i = 0; i < res->num_results; i++) {
printf("Browse result [%i](Source nodeid: %s, num_references: %i):\n",
i,
for (j = 0; j < res->results[i].num_references; j++) {
ref = &res->results[i].references[j];
printf(" Reference [%i]:\n", j);
printf(" Display name: %s\n", ua_string_const_data(&ref->display_name.text));
printf(" Target nodeid: %s\n", ua_nodeid_printable(&ref->node_id.id));
#ifdef ENABLE_TO_STRING
printf(" Nodeclass: %s\n", ua_nodeclass_to_string(ref->node_class));
#else
printf(" Nodeclass: %u\n", ref->node_class);
#endif
}
}
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECT;
return 0;
}

Disconnect

Disconnecting from the server involves multiple network messages, so the disconnect operation is asynchronous, too. Calling ua_client_begin_disconnect() is rather simple:

static void sample_client_disconnect(struct sample_client *ctx)
{
int ret;
ret = ua_client_begin_disconnect(&ctx->client, sample_client_disconnect_cb, ctx);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: ua_client_begin_disconnect failed with errorcode=%i\n", __func__, ret);
/* In case of a previous error the client might already have lost the connection
to the server. Then ua_client_begin_disconnect fails and the disconnect step
is skipped in the state machine. */
if (ctx->result != 0) ctx->result = ret;
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
} else {
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECTING;
}
}

The callback function checks the result, however the state is always changed to FINISH:

static void sample_client_disconnect_cb(struct ua_client *client, void *cb_data, int result, ua_statuscode statuscode)
{
struct sample_client *ctx = cb_data;
UA_UNUSED(client);
if (result != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: failed with errorcode=%i\n", __func__, result);
ctx->result = result;
} else if (statuscode != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: failed with statuscode=0x%08X\n", __func__, statuscode);
ctx->result = UA_EBADSERVICERESPONSE;
}
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
}

After disconnect the Sample Client provides a function to get the result:

int sample_client_get_result(struct sample_client *ctx)
{
return ctx->result;
}

And a function to cleanup:

void sample_client_cleanup(struct sample_client *ctx)
{
ua_client_cleanup(&ctx->client);
}

Step 3: main.c

The main.c contains the main function that is called at application startup. It parses the command line input, initializes the application, keeps the program running and cleans up.

First there are some global variables, the static variables could also be put at the beginnig of the main function or be dynamically allocated, but we do not want to waste stack space and keep dynamic allocations at a minimum. The g_appconfig needs to be global as it holds the configuration and is accessed by multiple components of the SDK.

struct appconfig g_appconfig;
static struct uaapplication g_app;
static struct sample_client g_ctx;

The main function starts with parsing command line agruments and initializing the uaapplication context and Sample Client context:

int facility_mask = TRACE_FAC_ALL;
int ret, opt;
static char url[128] = "opc.tcp://localhost:4840";
#ifdef HAVE_CRYPTO
static char config_file[UA_PATH_MAX] = "settings.conf";
#else
static char config_file[UA_PATH_MAX] = "settings_micro.conf";
#endif
struct sample_client *ctx = &g_ctx;
ctx->app = &g_app;
/* parse commandline arguments */
while ((opt = getopt(argc, argv, "hd:f:lc:u:")) != -1) {
switch (opt) {
case 'h':
usage(argv[0]);
exit(EXIT_SUCCESS);
case 'd':
trace_level = atoi(optarg);
break;
case 'f':
facility_mask = atoi(optarg);
break;
case 'c':
strncpy(config_file, optarg, sizeof(config_file) );
break;
case 'l':
print_debug_flags();
exit(EXIT_SUCCESS);
case 'u':
strncpy(url, optarg, sizeof(url) );
break;
default: /* '?' */
usage(argv[0]);
exit(EXIT_FAILURE);
}
}
client_init(trace_level, facility_mask, config_file);
ret = sample_client_init(ctx, url);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: sample_client_init failed with errorcode=%i\n", __func__, ret);
return EXIT_FAILURE;
}

Next is the main loop, from here all actions are run. To trigger actions in the SDK uaapplication_timed_docom() must be called regularly. It processes network message, encoding and is responsible for calling callback functions. For the Sample Client sample_client_check_state() must be called to progress in its state machine. The main loop runs until the Sample Client indicates it is finished.

while (ua_shutdown_should_shutdown() == 0) {
if (!sample_client_check_state(ctx)) break;
}

After that the result of the Sample Client is evaluated and the Sample CLient and uaaplication context is cleaned up. Some of the global variables are set to zero to help detecting memory leaks with tools like valgrind.

ret = sample_client_get_result(ctx);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: sample_client failed with result=%i\n", __func__, ret);
}
sample_client_cleanup(ctx);
client_cleanup();
if (ret == 0) return EXIT_SUCCESS;
else return EXIT_FAILURE;

Step 4: Run CMake and Build Application

To build a client application it is important to enable the cmake option ENABLE_CLIENT_FUNCTIONALITY. If your application is a standalone client without a server, the option ENABLE_SERVER_FUNCTIONALITY can be disabled.

CMake needs to know the source folder and the build folder for the project. The source folder is the lesson01 folder where the above mentioned files are stored (you can also use the higher level client_gettingstarted folder to build all lessons at once). The build folder is the folder where the executable will be built, e.g. a newly created folder inside or next to the source folder.

Depending on your platform you can choose one of the following options:

Console on Linux

  • Open a console and change the working directory to the source folder, then create the build folder:
    $ mkdir bld
  • Change the working directory to the build folder:
    $ cd bld
  • Run cmake with options like build type and the source folder as argument
    $ cmake -DCMAKE_BUILD_TYPE=Debug ..
  • Build the application using make
    $ make

Visual Studio on Windows

  • Open CMake (cmake-gui) from the start menu
  • Enter the source folder and build folder (the build folder does not need to exist yet)
  • Click the Configure button and choose your Visual Studio version
  • Click the Generate button
  • Open the generated solution in the build folder with Visual Studio
  • Use Visual Studio to build the project

Step 5: Run Application

To start the client the settings.conf file must be in the same folder as the built executable. Furthermore, the openssl dynamic libraries are needed. These must be either in the default search path of the system or also next to the executable.

Before starting the client a server must be running which allows the NONE security policy and Anonymous users. The client will by default connect to the url in the top of main.c (opc.tcp://localhost:4840). To connect to a different server the command line parameter -u can be used, for example '-u opc.tcp://my-server:48010'.