ANSI C Based OPC UA Client/Server SDK  1.8.0.369
Lesson 1: Setting up a Basic OPC UA Client Console Application

This lesson will guide you through the process of setting up a basic OPC UA Client console application.

Files used in this lesson:

Step 1: Create New Project

Set up a Console Application

Windows:

Create a new project. Use the following settings:

  • Win32 Console Application
  • No precompiled headers
  • Empty project.

Add Files

Add the file clientmain.c to your project.

Add Include Directories

Add the following include paths to your application (SDK_INSTALL_DIR is the installation folder of the SDK):

  • <SDK_INSTALL_DIR>/include/uastack
  • <SDK_INSTALL_DIR>/include/baselib
  • <SDK_INSTALL_DIR>/include/clientlib

Add Linker Settings

Windows:

For Additional Library Directories enter the following values:

  • <SDK_INSTALL_DIR>/lib
  • <SDK_INSTALL_DIR>/third-party/win32/[VisualStudioVersion]/openssl/out32dll.dbg (Debug)
  • <SDK_INSTALL_DIR>/third-party/win32/[VisualStudioVersion]/openssl/out32dll (Release)

For Additional Dependencies (Debug) enter:

  • uastackd.lib
  • baselibd.lib
  • clientlibd.lib
  • libeay32d.lib
  • ws2_32.lib

For Additional Dependencies (Release) enter:

  • uastack.lib
  • baselib.lib
  • clientlib.lib
  • libeay32.lib
  • ws2_32.lib

Linux:

For Additional Library Directories enter the following values:

  • -L<SDK_INSTALL_DIR>/lib

For Additional Dependencies (Debug) enter:

  • -lclientlibd -lbaselibd -luastackd -lssl

For Additional Dependencies (Release) enter:

  • -lclientlib -lbaselib -luastack -lssl
Note
Please remember that the link order is important for GCC!

Add Preprocessor Defines

Windows:

_CRT_SECURE_NO_WARNINGS

Set Output Path

Set output path to bin where the UA stack (Windows: and OpenSSL) libraries reside.

Enter these values:

  • Output Directory: <SDK_INSTALL_DIR>/bin

Step 2: Create Sample Client

Initialize UA Stack and Create the Main Function

The following code provides a generic main function where we will add the OPC UA specific code. The UA Stack requires global initialization before it can be used, for this purpose we use the helper function InitializeOpcUaStack. A default configuration is created and passed to the UA Stack initialization function of the SDK.

Warning
None of the SDK functionality can be used before the UA stack initialization is done.
This requirement includes static members or global variables using UA SDK or UA Stack classes and functions.
If UA SDK or UA Stack classes and functions are used before UaServer_Module_InitializeUaStack is called the server will crash since the platform layer of the UA stack is not loaded.
#include <uaclient_config.h>
#include <stdlib.h>
#include <opcua_datetime.h>
#include <opcua_trace.h>
#include <opcua_string.h>
#include <uaclient_module.h>
#include <uaclient_session.h>
/* Initializes OPC UA Stack.
* Here you can configure trace settings and other stack configuration options.
*/
OpcUa_StatusCode InitializeOpcUaStack(OpcUa_Handle *a_phProxyStubPlatformLayer,
OpcUa_ProxyStubConfiguration *a_pProxyStubConfiguration)
{
OpcUa_InitializeStatus(OpcUa_Module_Client, "InitializeOpcUaStack");
/* Initialize Stack */
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "UA Client: Initializing Stack...\n");
/* Default values can be changed here */
a_pProxyStubConfiguration->bProxyStub_Trace_Enabled = OpcUa_True;
a_pProxyStubConfiguration->uProxyStub_Trace_Level = OPCUA_TRACE_OUTPUT_LEVEL_ERROR;
uStatus = UaBase_Module_InitializeUaStack(a_phProxyStubPlatformLayer, a_pProxyStubConfiguration);
OpcUa_ReturnStatusCode;
OpcUa_BeginErrorHandling;
OpcUa_FinishErrorHandling;
}
/* Cleanup counterpart to InitializeOpcUaStack. */
OpcUa_StatusCode CleanupOpcUaStack(OpcUa_Handle *a_phProxyStubPlatformLayer)
{
OpcUa_InitializeStatus(OpcUa_Module_Client, "CleanupOpcUaStack");
/* Clean Up UA Stack */
uStatus = UaBase_Module_ClearUaStack(a_phProxyStubPlatformLayer);
OpcUa_ReturnStatusCode;
OpcUa_BeginErrorHandling;
OpcUa_FinishErrorHandling;
}
/* Main OPC UA Client Loop. */
OpcUa_StatusCode ClientMain()
{
OpcUa_InitializeStatus(OpcUa_Module_Client, "ClientMain");
OpcUa_ReturnStatusCode;
OpcUa_BeginErrorHandling;
OpcUa_FinishErrorHandling;
}
int main()
{
int ret = EXIT_SUCCESS;
OpcUa_StatusCode uStatus = OpcUa_Good;
OpcUa_Handle hProxyStubPlatformLayer = OpcUa_Null;
OpcUa_ProxyStubConfiguration proxyStubConfiguration;
/* Set up OPC UA */
uStatus = InitializeOpcUaStack(&hProxyStubPlatformLayer, &proxyStubConfiguration);
if ( OpcUa_IsNotGood(uStatus) )
{
return EXIT_FAILURE;
}
/* Start the main client loop */
uStatus = ClientMain();
if ( OpcUa_IsNotGood(uStatus) )
{
ret = EXIT_FAILURE;
}
/* Clean up OPC UA */
uStatus = CleanupOpcUaStack(&hProxyStubPlatformLayer);
if ( OpcUa_IsNotGood(uStatus) )
{
ret = EXIT_FAILURE;
}
return ret;
}

If the UaServer_Module_InitializeUaStack call succeeds, the UA SDK and UA Stack classes and functions can be used.

Sample Client Defines and Structures

#define UACLIENT_APPLICATION_NAME "UaSdkC - Client - Lesson01"
#define UACLIENT_APPLICATION_URI "urn:UnifiedAutomation:UaSdkC:Client:Lesson01"
#define UACLIENT_PRODUCT_URI "urn:UnifiedAutomation:UaSdkC:Client:Lesson01"
/* Server configuration used by this client */
#define SERVER_ENDPOINT_URL "opc.tcp://localhost:48020"

These defines determine the name and URI the Client will report to the server when connecting. The SERVER_ENDPOINT_URL is the URL of the OPC UA Server to connect to.

typedef enum _SampleStateMachine
{
State_Idle,
State_Connect,
State_Connected,
State_Read,
State_ReadDone,
State_Disconnect,
State_Disconnected,
State_Error
} SampleStateMachine;
typedef struct _SampleClientContext
{
SampleStateMachine State;
} SampleClientContext;

The Client SDK offers asynchronous functions for blocking operations, so the sample client uses a state machine to track its own state and determine the next operation. The SampleStateMachine starts with the Idle state and is processed sequentially. The states Connect, Read and Disconnect are entered when the main loop starts the corresponding operation. The states Connected, ReadDone and Disconnected are entered by the callback functions when the corresponding operation finishes successfully. The Disconnected state is the final state in case everything works fine. The Error state may be entered from any state and is the final state in case of an error.

To access the state machine from within the callbacks, the sample client has the SampleClientContext, which will be attached to the session as UserData and thus passed to the callback functions.

Callback Functions

To handle the result of asynchronous operations or react to events, the client needs to implement a few callback functions.

ConnectionStatusChanged Callback

OpcUa_Void Sample_ConnectionStatusChanged_CB(UaClient_Session *a_pSession,
{
SampleClientContext *pClientContext = a_pSession->pUserData;
const char *pStatus = "INVALID";
switch (a_status)
{
pStatus = "Disconnected";
if (pClientContext->State == State_Connect)
{
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "UA Client: failed to connect to server!\n");
}
if (pClientContext->State == State_Disconnect)
{
pClientContext->State = State_Disconnected;
}
else
{
pClientContext->State = State_Error;
}
break;
pStatus = "Connected";
if (pClientContext->State == State_Connect)
{
pClientContext->State = State_Connected;
}
else
{
pClientContext->State = State_Error;
}
break;
pStatus = "Connecting";
break;
pStatus = "ConnectionWarningWatchdogTimeout";
break;
pStatus = "ConnectionErrorClientReconnect";
break;
pStatus = "SessionAutomaticallyRecreated";
break;
default: break;
}
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "\n--> Sample_ConnectionStatusChanged_CB: %s\n\n", pStatus);
}

Sample_ConnectionStatusChanged_CB implements the UaClient_Session_ConnectionStatusChanged_CB callback function. It handles the state transitions from Connect to Connected and Disconnect to Disconnected.

ConnectError Callback

OpcUa_Boolean Sample_ConnectError_CB(UaClient_Session *a_pSession,
OpcUa_Boolean a_overridable)
{
SampleClientContext *pClientContext = a_pSession->pUserData;
const char *pServiceType = "INVALID";
switch (a_serviceType)
{
case UaClient_ConnectServiceType_CertificateValidation: pServiceType = "CertificateValidation"; break;
case UaClient_ConnectServiceType_OpenSecureChannel: pServiceType = "OpenSecureChannel"; break;
case UaClient_ConnectServiceType_CreateSession: pServiceType = "CreateSession"; break;
case UaClient_ConnectServiceType_UserIdentityToken: pServiceType = "UserIdentityToken"; break;
case UaClient_ConnectServiceType_ActivateSession: pServiceType = "ActivateSession"; break;
default: break;
}
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "Sample_ConnectError_CB:\n");
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, " ServiceType: %s\n", pServiceType);
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, " Error: 0x%08x\n", a_error);
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, " Overridable: %s\n", a_overridable == OpcUa_False ? "false" : "true");
pClientContext->State = State_Error;
return OpcUa_False;
}

Sample_ConnectError_CB implements the UaClient_Session_ConnectError_CB callback function. This callback is only called on errors, so it enters the Error state.

Read Callback

OpcUa_Void Sample_Read_CB(const UaClient_Session *a_pSession,
OpcUa_ResponseHeader *a_pResponseHeader,
OpcUa_Int32 a_NoOfResults,
OpcUa_DataValue *a_pResults,
OpcUa_Int32 a_NoOfDiagnosticInfos,
OpcUa_DiagnosticInfo *a_pDiagnosticInfos,
OpcUa_Void *a_pUserData)
{
SampleClientContext *pClientContext = a_pSession->pUserData;
OpcUa_ReferenceParameter(a_NoOfDiagnosticInfos);
OpcUa_ReferenceParameter(a_pDiagnosticInfos);
OpcUa_ReferenceParameter(a_pUserData);
if (OpcUa_IsGood(a_pResponseHeader->ServiceResult))
{
OpcUa_Int32 i;
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "Sample_Read_CB:\n");
for (i = 0; i < a_NoOfResults; i++)
{
char szSourceTimestamp[64] = {0};
char szServerTimestamp[64] = {0};
char szValue[64] = {0};
OpcUa_DateTime_GetStringFromDateTime(a_pResults[i].SourceTimestamp, szSourceTimestamp, sizeof(szSourceTimestamp));
OpcUa_DateTime_GetStringFromDateTime(a_pResults[i].ServerTimestamp, szServerTimestamp, sizeof(szServerTimestamp));
Variant_ToString(&a_pResults[i].Value, szValue, sizeof(szValue));
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, " [%i]:\n", i);
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, " Status: 0x%08x\n", a_pResults[i].StatusCode);
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, " SourceTimestamp: %s\n", szSourceTimestamp);
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, " ServerTimestamp: %s\n", szServerTimestamp);
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, " Value: %s\n", szValue);
}
if (pClientContext->State == State_Read)
pClientContext->State = State_ReadDone;
else
pClientContext->State = State_Error;
}
else
{
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "Sample_Read_CB failed (0x%08x)\n", a_pResponseHeader->ServiceResult);
pClientContext->State = State_Error;
}
}

Sample_Read_CB implements the callback for the Read service and is called with the result of the service invocation. This sample implementation checks whether the result is good and prints the received values. It also handles the state transition from Read to ReadDone.

Client Main

The following code is used to initialize the client object and create a session that is connected to an OPC UA server and reads the values of two nodes.

Variable Declarations

UaClient uaClient;
OpcUa_Boolean bClientInitialized = OpcUa_False;
UaClient_Configuration *pClientConfiguration = OpcUa_Null;
UaClient_Session *pSession = OpcUa_Null;
UaClient_Session_Callback sessionCallback;
OpcUa_Boolean bComplete = OpcUa_False;
SampleClientContext clientContext;

These variables are valid till the client shuts down, so resources like UaClient or the SampleClientContext can be allocated on the stack and don’t need dynamic memory allocation.

Initialize Client Context

clientContext.State = State_Idle;

Set the beginning state of the client to Idle.

Set Configuration

/* Initialize the Client SDK */
uStatus = UaClient_Initialize(&uaClient);
OpcUa_GotoErrorIfBad(uStatus);
bClientInitialized = OpcUa_True;
pClientConfiguration = UaClient_GetConfiguration();
/* ApplicationDescription and BuildInfo are not set by UaClient_Settings_GetConfigurationFromSettings,
the application has to set those manually */
/* ApplicationDescription */
OpcUa_String_AttachReadOnly(&pClientConfiguration->ApplicationDescription.ApplicationUri, UACLIENT_APPLICATION_URI);
OpcUa_String_AttachReadOnly(&pClientConfiguration->ApplicationDescription.ProductUri, UACLIENT_PRODUCT_URI);
OpcUa_String_AttachReadOnly(&pClientConfiguration->ApplicationDescription.ApplicationName.Text, UACLIENT_APPLICATION_NAME);
/* This example does not use security, disable PKI */
pClientConfiguration->PkiConfig.strPkiType = (char*)OPCUA_PKI_TYPE_NONE;

Initialize the UaClient and retrieve its default configuration to set application details as defined above. To keep this Sample Client simple, it uses security policy NONE and does not authenticate the server or cryptographically sign or encrypt any messages.

Note
For a final product it is strongly recommended to NOT use the security policy NONE!

Create Session

/* Create an OPC UA Session which handles the connection */
OpcUa_MemSet(&sessionCallback, 0, sizeof(sessionCallback));
sessionCallback.pfConnectionStatusChanged_CB = Sample_ConnectionStatusChanged_CB;
sessionCallback.pfConnectError_CB = Sample_ConnectError_CB;
uStatus = UaClient_Session_Create(&sessionCallback, &pSession);
OpcUa_GotoErrorIfBad(uStatus);
/* Set EndpointUrl */
OpcUa_String_AttachReadOnly(&pSession->EndpointDescription.EndpointUrl, SERVER_ENDPOINT_URL);
/* Disable automatic reconnect */
pSession->AutomaticReconnect = OpcUa_False;
/* Set user data */
pSession->pUserData = &clientContext;

The UaClient_Session represents the client side of a session. UaClient_Session_Create only allocates required resources, but does not create a session on the server. Upon session creation, the session callback functions are passed to receive asynchronous notifications on session changes. Furthermore, the user data is set to obtain the clientContext in the callbacks.

StartUp Client

/* StartUp Client */
uStatus = UaClient_StartUp(&uaClient);
OpcUa_GotoErrorIfBad(uStatus);

Main Loop

/* Main loop */
while (!(bComplete && pSession->ConnectionStatus == UaClient_ConnectionStatus_Disconnected))
{
/* process sample state machine */
switch (clientContext.State)
{
case State_Idle:
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "UA Client: Connecting to %s ...\n", SERVER_ENDPOINT_URL);
uStatus = UaClient_Session_BeginConnect(pSession);
OpcUa_GotoErrorIfBad(uStatus);
clientContext.State = State_Connect;
break;
case State_Connected:
uStatus = Sample_Read(pSession);
OpcUa_GotoErrorIfBad(uStatus);
clientContext.State = State_Read;
break;
case State_ReadDone:
uStatus = UaClient_Session_BeginDisconnect(pSession, OpcUa_False);
OpcUa_GotoErrorIfBad(uStatus);
clientContext.State = State_Disconnect;
break;
case State_Disconnected:
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "Sample successfully completed. Terminating now.\n");
bComplete = OpcUa_True;
break;
case State_Error:
uStatus = UaClient_Session_BeginDisconnect(pSession, OpcUa_True);
if (OpcUa_IsBad(uStatus) && uStatus != OpcUa_BadInvalidState) {OpcUa_GotoError;}
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "An error occured. Terminating now.\n");
bComplete = OpcUa_True;
break;
default:
break;
}
/* Process Ua Client events */
uStatus = UaBase_DoCom();
if (OpcUa_IsBad(uStatus))
{
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "UaBase_DoCom failed (0x%08x)\n", uStatus);
bComplete = OpcUa_True;
}
}

The main loop processes the state machine. UaClient_Session_BeginConnect is called to establish a network connection to a UA Server, create a session on the server, and activate the session. Sample_Read is a local function to read values from the server. It is explained in detail further below. UaClient_Session_BeginDisconnect closes the session on the server and tears down the network connection.

These functions all require network I/O to finish, so they only start an operation. To guarantee progress, UaBase_DoCom must be called regularly.

Clean up Session

Clears the local resources for the session.

Client Shutdown

/* UaClient_Clear clears the PkiConfig of the client and attempts to free the strings
set there. As we have set literal string constants, we don't want those to be freed, so
we set them to NULL by initializing the structure. */
OpcUa_CertificateStoreConfiguration_Initialize(&pClientConfiguration->PkiConfig);
/* Clean Up Client */

Read Service Invocation

OpcUa_StatusCode Sample_Read(UaClient_Session *pSession)
{
OpcUa_Int32 numNodesToRead = 2;
OpcUa_ReadValueId nodesToRead[2];
OpcUa_ReadValueId_Initialize(&nodesToRead[0]);
nodesToRead[0].AttributeId = OpcUa_Attributes_Value;
nodesToRead[0].NodeId.NamespaceIndex = 0;
nodesToRead[0].NodeId.Identifier.Numeric = OpcUaId_Server_ServerStatus_State;
OpcUa_ReadValueId_Initialize(&nodesToRead[1]);
nodesToRead[1].AttributeId = OpcUa_Attributes_Value;
nodesToRead[1].NodeId.NamespaceIndex = 0;
nodesToRead[1].NodeId.Identifier.Numeric = OpcUaId_Server_ServerStatus_CurrentTime;
return UaClient_Session_BeginRead(pSession, /* Session */
OpcUa_Null, /* ServiceSettings */
0, /* MaxAge */
OpcUa_TimestampsToReturn_Both, /* TimestampsToReturn */
numNodesToRead, /* NoOfNodesToRead */
nodesToRead, /* NodesToRead */
Sample_Read_CB, /* Callback Function */
OpcUa_Null); /* Callback UserData */
}

To invoke the Read Service, the parameters for the Read Service as defined in the OPC UA Specification are passed to UaClient_Session_BeginRead. The Request Header is omitted because it is handled by the SDK; to influence its values, the UaClient_ServiceSettings can be given as an argument.

Additional arguments are the session, which must be connected to the server, the callback function to call upon completion, and user data. The user data is passed only to the callback of this particular service invocation, allowing to distinguish multiple concurrent calls to the same service using the same callback function. This Sample Client does not need to pass user data this way, as it only requires the user data attached to the session. The session is also available in the callback and thus its user data.

Step 3: Run Application

First, start an OPC UA Server like the ANSI C SDK Demo Server and make sure that the Endpoint Url is identical to the SERVER_ENDPOINT_URL define in clientmain.c.

When starting the Sample Client, you should see the values read from the server. The ServerStatus_State should be zero for a running server and the ServerStatus_CurrentTime the current time of the server (see screenshot).

Note
All time values are shown in UTC and may deviate from your local time.
clientgettingstarted1_readvalues.png