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:
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:
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>
OpcUa_ProxyStubConfiguration *a_pProxyStubConfiguration)
{
OpcUa_InitializeStatus(OpcUa_Module_Client, "InitializeOpcUaStack");
printf("UA Client: Initializing Stack...\n");
a_pProxyStubConfiguration->bProxyStub_Trace_Enabled = OpcUa_True;
a_pProxyStubConfiguration->uProxyStub_Trace_Level = OPCUA_TRACE_OUTPUT_LEVEL_WARNING;
OpcUa_ReturnStatusCode;
OpcUa_BeginErrorHandling;
OpcUa_FinishErrorHandling;
}
{
OpcUa_InitializeStatus(OpcUa_Module_Client, "CleanupOpcUaStack");
OpcUa_ReturnStatusCode;
OpcUa_BeginErrorHandling;
OpcUa_FinishErrorHandling;
}
{
OpcUa_InitializeStatus(OpcUa_Module_Client, "ClientMain");
OpcUa_ReturnStatusCode;
OpcUa_BeginErrorHandling;
OpcUa_FinishErrorHandling;
}
int main(void)
{
int ret = EXIT_SUCCESS;
OpcUa_Handle hProxyStubPlatformLayer = OpcUa_Null;
OpcUa_ProxyStubConfiguration proxyStubConfiguration;
uStatus = InitializeOpcUaStack(&hProxyStubPlatformLayer, &proxyStubConfiguration);
if ( OpcUa_IsNotGood(uStatus) )
{
return EXIT_FAILURE;
}
uStatus = ClientMain();
if ( OpcUa_IsNotGood(uStatus) )
{
ret = EXIT_FAILURE;
}
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"
#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
{
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 a_overridable)
{
SampleClientContext *pClientContext = a_pSession->
pUserData;
const char *pServiceType = "INVALID";
OpcUa_ReferenceParameter(a_error);
OpcUa_ReferenceParameter(a_overridable);
switch (a_serviceType)
{
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. For overriding of overridable errors, the callback must be modified to return OpcUa_True to the SDK.
Read Callback
OpcUa_Int32 a_NoOfResults,
OpcUa_Int32 a_NoOfDiagnosticInfos,
OpcUa_Void *a_pUserData)
{
SampleClientContext *pClientContext = a_pSession->
pUserData;
OpcUa_ReferenceParameter(a_NoOfDiagnosticInfos);
OpcUa_ReferenceParameter(a_pDiagnosticInfos);
OpcUa_ReferenceParameter(a_pUserData);
{
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.
The sample code handles only simple OPC UA Data Types. There is extended sample code available for Handling structure DataTypes.
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
OpcUa_Boolean bClientInitialized = OpcUa_False;
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
OpcUa_GotoErrorIfBad(uStatus);
bClientInitialized = OpcUa_True;
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
OpcUa_MemSet(&sessionCallback, 0, sizeof(sessionCallback));
sessionCallback.pfConnectionStatusChanged_CB = Sample_ConnectionStatusChanged_CB;
sessionCallback.pfConnectError_CB = Sample_ConnectError_CB;
OpcUa_GotoErrorIfBad(uStatus);
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
OpcUa_GotoErrorIfBad(uStatus);
Main Loop
{
switch (clientContext.State)
{
case State_Idle:
OpcUa_Trace(OPCUA_TRACE_LEVEL_WARNING, "UA Client: Connecting to %s ...\n", SERVER_ENDPOINT_URL);
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:
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:
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;
}
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
OpcUa_CertificateStoreConfiguration_Initialize(&pClientConfiguration->
PkiConfig);
Read Service Invocation
{
OpcUa_Int32 numNodesToRead = 2;
OpcUa_ReadValueId_Initialize(&nodesToRead[0]);
nodesToRead[0].
NodeId.Identifier.Numeric = OpcUaId_Server_ServerStatus_State;
OpcUa_ReadValueId_Initialize(&nodesToRead[1]);
nodesToRead[1].
NodeId.Identifier.Numeric = OpcUaId_Server_ServerStatus_CurrentTime;
OpcUa_Null,
0,
numNodesToRead,
nodesToRead,
Sample_Read_CB,
OpcUa_Null);
}
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.