Step 1: Browse for Nodes in the Address Space
In this step, we will create two methods for browsing the server’s address space, browseSimple and browseContinuationPoint. browseSimple browses the address space from a starting node and returns all references at once. browseContinuationPoint is more suitable for dealing with a larger number of references. They are not returned at once but split into smaller portions. Note that even if the client does not limit the number of references to return, the server can still revise that number and return the references in multiple smaller portions.
Add the following code to sampleclient.h
...
private:
UaStatus browseInternal(
const UaNodeId& nodeToBrowse, OpcUa_UInt32 maxReferencesToReturn);
...
Add the following code snippets to sampleclient.cpp
{
nodeToBrowse =
UaNodeId(OpcUaId_RootFolder);
result = browseInternal(nodeToBrowse, 0);
return result;
}
UaStatus SampleClient::browseContinuationPoint()
{
result = browseInternal(nodeToBrowse, 5);
return result;
}
Both methods use the helper method browseInternal which contains the actual browse service call.
Add the following code to sampleclient.cpp:
UaStatus SampleClient::browseInternal(
const UaNodeId& nodeToBrowse, OpcUa_UInt32 maxReferencesToReturn)
{
ServiceSettings serviceSettings;
browseContext.referenceTypeId = OpcUaId_HierarchicalReferences;
browseContext.includeSubtype = OpcUa_True;
browseContext.maxReferencesToReturn = maxReferencesToReturn;
result = m_pSession->browse(
serviceSettings,
nodeToBrowse,
browseContext,
continuationPoint,
referenceDescriptions);
if (result.isGood())
{
printBrowseResults(referenceDescriptions);
while (continuationPoint.
length() > 0)
{
printf("\nContinuationPoint is set. BrowseNext...\n");
result = m_pSession->browseNext(
serviceSettings,
OpcUa_False,
continuationPoint,
referenceDescriptions);
if (result.isGood())
{
printBrowseResults(referenceDescriptions);
}
else
{
printf("BrowseNext failed with status %s\n", result.toString().toUtf8());
}
}
}
else
{
printf("Browse failed with status %s\n", result.toString().toUtf8());
}
return result;
}
browseInternal takes two input arguments. nodeToBrowse is the NodeId of the starting Node and maxReferencesToReturn the maximum number of references returned in the browse response.
browseContext contains the filter settings for the browse call. It includes the maximum number of references to return, which is set to zero in browseSimple (no limitation, i.e. all references are returned at once).
The method calls UaSession::browse. If not all references can be delivered at once (in our case because maxReferencesToReturn is exceeded) a continuation point is set and returned in the browse response. To receive the remaining references, browseNext is called repeatedly using the continuation point returned in the previous browse or browseNext call.
The references returned are displayed using the helper method printBrowseResults:
{
OpcUa_UInt32 i;
for (i=0; i<referenceDescriptions.length(); i++)
{
printf("node: ");
UaNodeId referenceTypeId(referenceDescriptions[i].ReferenceTypeId);
printf("[Ref=%s] ", referenceTypeId.toString().toUtf8() );
printf("%s ( ", browseName.toString().toUtf8() );
if (referenceDescriptions[i].NodeClass & OpcUa_NodeClass_Object) printf("Object ");
if (referenceDescriptions[i].NodeClass & OpcUa_NodeClass_Variable) printf("Variable ");
if (referenceDescriptions[i].NodeClass & OpcUa_NodeClass_Method) printf("Method ");
if (referenceDescriptions[i].NodeClass & OpcUa_NodeClass_ObjectType) printf("ObjectType ");
if (referenceDescriptions[i].NodeClass & OpcUa_NodeClass_VariableType) printf("VariableType ");
if (referenceDescriptions[i].NodeClass & OpcUa_NodeClass_ReferenceType) printf("ReferenceType ");
if (referenceDescriptions[i].NodeClass & OpcUa_NodeClass_DataType) printf("DataType ");
if (referenceDescriptions[i].NodeClass & OpcUa_NodeClass_View) printf("View ");
UaNodeId nodeId(referenceDescriptions[i].NodeId.NodeId);
printf("[NodeId=%s] ", nodeId.toFullString().toUtf8() );
printf(")\n");
}
}
Now we are ready to call browseSimple and browseContinuationPoint from main:
Replace the following code in client_cpp_sdk_tutorial.cpp
{
status = pMyClient->read();
printf("\nPress Enter to create subscription\n");
printf("Press Enter again to stop subscription\n");
getchar();
status = pMyClient->subscribe();
{
getchar();
status = pMyClient->unsubscribe();
}
printf("\nPress Enter to disconnect\n");
getchar();
status = pMyClient->disconnect();
}
with
{
printf("\nPress Enter to do a simple browse\n");
getchar();
status = pMyClient->browseSimple();
printf("\nPress Enter to browse with continuation point\n");
getchar();
status = pMyClient->browseContinuationPoint();
printf("\nPress Enter to disconnect\n");
getchar();
status = pMyClient->disconnect();
}
Step 2: Providing Connection Parameters and NodeIds Using a Configuration Object
As a next step, we will replace the hardcoded connection parameters with information loaded from the configuration file sampleconfig.ini which can be found in [SDK Installation Directory]/bin. Later on, nodes to read and write will also be loaded from this file.
Add a new header file named configuration.h containing the following code to your project:
#ifndef CONFIGURATION_H
#define CONFIGURATION_H
#include "uabase.h"
#include "uaclientsdk.h"
#define COMPANY_NAME "UnifiedAutomation"
#define PRODUCT_NAME "GettingStartedClient"
class Configuration
{
UA_DISABLE_COPY(Configuration);
public:
Configuration();
virtual ~Configuration();
OpcUa_Boolean getAutomaticReconnect() const;
OpcUa_Boolean getRetryInitialConnect() const;
private:
OpcUa_Boolean m_bAutomaticReconnect;
OpcUa_Boolean m_bRetryInitialConnect;
};
#endif // CONFIGURATION_H
Add a new file named configuration.cpp to your project.
#include "configuration.h"
#include "uasettings.h"
Configuration::Configuration()
{
}
Configuration::~Configuration()
{
}
The constructor and destructor remain empty.
We implement getter methods for each parameter to load:
UaString Configuration::getServerUrl()
const
{
return m_serverUrl;
}
UaString Configuration::getDiscoveryUrl()
const
{
return m_discoveryUrl;
}
UaString Configuration::getApplicationName()
const
{
return m_applicationName;
}
OpcUa_Boolean Configuration::getAutomaticReconnect() const
{
return m_bAutomaticReconnect;
}
OpcUa_Boolean Configuration::getRetryInitialConnect() const
{
return m_bRetryInitialConnect;
}
The method loadConfiguration uses the UaSettings class to load parameters from an ini file. Default values are provided as well.
{
value = pSettings->
value(
"DiscoveryURL",
UaString(
"opc.tcp://localhost:48010"));
value = pSettings->
value(
"ServerUrl",
UaString(
"opc.tcp://localhost:48010"));
value = pSettings->
value(
"AutomaticReconnect",
UaVariant((OpcUa_Boolean)OpcUa_True));
value.
toBool(m_bAutomaticReconnect);
value = pSettings->
value(
"RetryInitialConnect",
UaVariant((OpcUa_Boolean)OpcUa_False));
value.
toBool(m_bRetryInitialConnect);
delete pSettings;
pSettings = NULL;
return result;
}
Add the following code to sampleclient.cpp:
#include "sampleclient.h"
#include "uasession.h"
#include "samplesubscription.h"
#include "configuration.h"
SampleClient::SampleClient()
{
m_pSampleSubscription = new SampleSubscription();
m_pConfiguration = new Configuration();
}
SampleClient::~SampleClient()
{
if (m_pSampleSubscription)
{
...
}
if (m_pSession)
{
...
}
if (m_pConfiguration)
{
delete m_pConfiguration;
m_pConfiguration = NULL;
}
}
Add the following code to sampleclient.h
#ifndef SAMPLECLIENT_H
#define SAMPLECLIENT_H
#include "uabase.h"
#include "uaclientsdk.h"
class SampleSubscription;
class Configuration;
{
UA_DISABLE_COPY(SampleClient);
public:
SampleClient();
virtual ~SampleClient();
void setConfiguration(Configuration* pConfiguration);
...
private:
SampleSubscription* m_pSampleSubscription;
Configuration* m_pConfiguration;
Implement setConfiguration:
void SampleClient::setConfiguration(Configuration* pConfiguration)
{
if (m_pConfiguration)
{
delete m_pConfiguration;
}
m_pConfiguration = pConfiguration;
}
Replace the existing method SampleClient::connect with the following code to replace the previously hardcoded information with data loaded from the configuration. See SessionConnectInfo for a parameter description.
{
SessionConnectInfo sessionConnectInfo;
char szHostName[256];
if (0 == UA_GetHostname(szHostName, 256))
{
sNodeName = szHostName;
}
sessionConnectInfo.sApplicationName = m_pConfiguration->getApplicationName();
sessionConnectInfo.sApplicationUri =
UaString(
"urn:%1:%2:%3").
arg(sNodeName).
arg(COMPANY_NAME).
arg(PRODUCT_NAME);
sessionConnectInfo.sProductUri =
UaString(
"urn:%1:%2").
arg(COMPANY_NAME).
arg(PRODUCT_NAME);
sessionConnectInfo.sSessionName = sessionConnectInfo.sApplicationUri;
sessionConnectInfo.bAutomaticReconnect = m_pConfiguration->getAutomaticReconnect();
sessionConnectInfo.bRetryInitialConnect = m_pConfiguration->getRetryInitialConnect();
SessionSecurityInfo sessionSecurityInfo;
printf("\nConnecting to %s\n", m_pConfiguration->getServerUrl().toUtf8());
m_pConfiguration->getServerUrl(),
sessionConnectInfo,
sessionSecurityInfo,
this);
if (result.isGood())
{
printf("Connect succeeded\n");
}
else
{
printf("Connect failed with status %s\n", result.toString().toUtf8());
}
return result;
}
Add the following files from [SDK Installation Directory]/examples/utilities to your project:
Add the following code to client_cpp_sdk_tutorial.cpp to create a configuration object and finally load the configuration from the file sampleconfig.ini.
#include "uaplatformlayer.h"
#include "sampleclient.h"
#include "configuration.h"
#include "shutdown.h"
...
SampleClient* pMyClient = new SampleClient();
sConfigFile += "/sampleconfig.ini";
Configuration* pMyConfiguration = new Configuration();
status = pMyConfiguration->loadConfiguration(sConfigFile);
{
pMyClient->setConfiguration(pMyConfiguration);
status = pMyClient->connect();
}
else
{
delete pMyConfiguration;
pMyConfiguration = NULL;
}
Step 3: Read namespace table
In addition to the connection information, we will use the configuration file to load nodes to read and write. For this purpose, we are reading the namespace table after connect to set up the namespace index for NodeIds from the configuration object.
Add the following code to configuration.h
...
private:
OpcUa_Boolean m_bAutomaticReconnect;
OpcUa_Boolean m_bRetryInitialConnect;
};
Add the following code to loadConfiguration to read the namespace array from the configuration file (documentation for UaSettings).
{
OpcUa_UInt32 i = 0;
OpcUa_UInt32 size = 0;
...
value = pSettings->
value(
"AutomaticReconnect",
UaVariant((OpcUa_Boolean)OpcUa_True));
value.
toBool(m_bAutomaticReconnect);
value = pSettings->
value(
"RetryInitialConnect",
UaVariant((OpcUa_Boolean)OpcUa_False));
value.
toBool(m_bRetryInitialConnect);
m_namespaceArray.clear();
value = pSettings->
value(
"size", (OpcUa_UInt32)0);
m_namespaceArray.resize(size);
for (i=0; i<size;i++)
{
}
delete pSettings;
pSettings = NULL;
return result;
}
Add the method updateNamespaceIndexes to configuration.cpp:
{
OpcUa_UInt32 i, j;
OpcUa_UInt32 size;
size = m_namespaceArray.length();
for (i = 0; i < m_namespaceArray.length(); i++)
{
mappingTable[i] = (OpcUa_UInt16)i;
for (j = 0; j < namespaceArray.length(); j++)
{
if (string1 == string2)
{
mappingTable[i] = (OpcUa_UInt16)j;
break;
}
}
}
m_namespaceArray = namespaceArray;
return result;
}
This method creates a mapping table to map the namespace indexes from the configuration file to the namespace indexes on the server. This is necessary because the namespace indexes on the server may change after a restart of the server but the namespace URIs are always valid.
Add the following code to sampleclient.h
private:
SampleSubscription* m_pSampleSubscription;
Configuration* m_pConfiguration;
UaClient::ServerStatus m_serverStatus;
};
#endif // SAMPLECLIENT_H
Extend connectionStatusChanged to read the namespace table after connect (documentation for getNamespaceTable).
void SampleClient::connectionStatusChanged(
OpcUa_UInt32 clientConnectionId,
UaClient::ServerStatus serverStatus)
{
OpcUa_ReferenceParameter(clientConnectionId);
printf("-------------------------------------------------------------\n");
switch (serverStatus)
{
case UaClient::Disconnected:
printf("Connection status changed to Disconnected\n");
break;
case UaClient::Connected:
printf("Connection status changed to Connected\n");
if (m_serverStatus != UaClient::NewSessionCreated)
{
}
break;
case UaClient::ConnectionWarningWatchdogTimeout:
printf("Connection status changed to ConnectionWarningWatchdogTimeout\n");
break;
case UaClient::ConnectionErrorApiReconnect:
printf("Connection status changed to ConnectionErrorApiReconnect\n");
break;
case UaClient::ServerShutdown:
printf("Connection status changed to ServerShutdown\n");
break;
case UaClient::NewSessionCreated:
printf("Connection status changed to NewSessionCreated\n");
break;
}
printf("-------------------------------------------------------------\n");
m_serverStatus = serverStatus;
}
Step 4: Enhance read Method to Use NodeIds from Configuration Object
In this step, the read method is enhanced to use NodeIds loaded from the configuration file.
First, we add the new getter method getNodesToRead.
Add the following code to configuration.h
...
OpcUa_Boolean getAutomaticReconnect() const;
OpcUa_Boolean getRetryInitialConnect() const;
...
private:
...
};
#endif // CONFIGURATION_H
Implement the method getNodesToRead in configuration.cpp:
{
return m_nodesToRead;
}
The method Configuration::loadConfiguration has to be extended to read the NodeIds from the configuration file (documentation for UaSettings).
...
m_namespaceArray.clear();
value = pSettings->
value(
"size", (OpcUa_UInt32)0);
m_namespaceArray.resize(size);
for (i=0; i<size;i++)
{
}
value = pSettings->
value(
"size", (OpcUa_UInt32)0);
m_nodesToRead.resize(size);
for (i=0; i<size;i++)
{
}
...
Add the following code to Configuration::updateNamespaceIndexes to replace the namespace index in NodeIds.
...
for (i = 0; i < m_nodesToRead.length(); i++)
{
m_nodesToRead[i].NamespaceIndex = mappingTable[m_nodesToRead[i].NamespaceIndex];
}
m_namespaceArray = namespaceArray;
return result;
}
Then read has to be modified to use the NodeIds from configuration instead of the single hardcoded one from the first lesson.
Replace the existing read method with the following code:
{
ServiceSettings serviceSettings;
UaDiagnosticInfos diagnosticInfos;
nodesToRead.
create(nodes.length());
for (OpcUa_UInt32 i = 0; i < nodes.length(); i++)
{
nodesToRead[i].AttributeId = OpcUa_Attributes_Value;
OpcUa_NodeId_CopyTo(&nodes[i], &nodesToRead[i].NodeId);
}
printf("\nReading ...\n");
result = m_pSession->
read(
serviceSettings,
0,
nodesToRead,
values,
diagnosticInfos);
{
for (OpcUa_UInt32 i = 0; i < nodes.length(); i++)
{
if (OpcUa_IsGood(values[i].StatusCode))
{
printf(
"Value[%d]: %s\n", i,
UaVariant(values[i].Value).toString().toUtf8());
}
else
{
printf(
"Read failed for item[%d] with status %s\n", i,
UaStatus(values[i].StatusCode).toString().toUtf8());
}
}
}
else
{
}
return result;
}
Now we are ready to call read from main:
...
{
printf("\nPress Enter to read values\n");
getchar();
status = pMyClient->read();
...
Step 5: Writing a Value
The last step in this lesson shows how to write the value attribute of a variable.
The nodes to write are loaded from the configuration file. For this purpuse we are adding two getter methods.
Add the following code to configuration.h
Implement the methods getNodesToWrite and getWriteValues in configuration.cpp.
{
return m_nodesToWrite;
}
{
return m_writeValues;
}
We have to extend loadConfiguration to read the NodeIds, DataTypes of the Nodes and the values to write from the configuration file.
{
OpcUa_UInt32 i = 0;
OpcUa_UInt32 size = 0;
OpcUa_Byte byte;
...
m_nodesToRead.clear();
value = pSettings->
value(
"size", (OpcUa_UInt32)0);
m_nodesToRead.resize(size);
for (i=0; i<size;i++)
{
}
value = pSettings->
value(
"size", (OpcUa_UInt32)0);
m_nodesToWrite.resize(size);
for (i=0; i<size;i++)
{
}
for (i=0; i<size;i++)
{
writeDataTypes[(int)i] = byte;
}
for (i=0; i<size;i++)
{
OpcUa_BuiltInType type = (OpcUa_BuiltInType)(char)writeDataTypes[(int)i];
if (OpcUa_IsGood(value.
changeType(type, OpcUa_False)))
{
value.
copyTo(&m_writeValues[i]);
}
else
{
printf(
"Cannot convert variant value: %s\n", value.
toString().
toUtf8());
}
}
Configuration::updateNamespaceIndexes has to be extended to replace the namespace index in NodeIds of the nodes to write.
...
for (i = 0; i < m_nodesToRead.length(); i++)
{
m_nodesToRead[i].NamespaceIndex = mappingTable[m_nodesToRead[i].NamespaceIndex];
}
for (i = 0; i < m_nodesToWrite.length(); i++)
{
m_nodesToWrite[i].NamespaceIndex = mappingTable[m_nodesToWrite[i].NamespaceIndex];
}
...
We need to implement the method write which uses UaSession::write to write the values.
Add method write to sampleclient.h
...
UaStatus connect();
...
Then we implement write
{
ServiceSettings serviceSettings;
UaDiagnosticInfos diagnosticInfos;
nodesToWrite.
create(nodes.length());
for (OpcUa_UInt32 i = 0; i < nodes.length(); i++)
{
nodesToWrite[i].AttributeId = OpcUa_Attributes_Value;
OpcUa_NodeId_CopyTo(&nodes[i], &nodesToWrite[i].NodeId);
OpcUa_Variant_CopyTo(&values[i], &nodesToWrite[i].Value.Value);
}
printf("\nWriting...\n");
result = m_pSession->
write(
serviceSettings,
nodesToWrite,
results,
diagnosticInfos);
{
for (OpcUa_UInt32 i = 0; i < results.length(); i++)
{
if (OpcUa_IsGood(results[i]))
{
printf("Write succeeded for item[%d]\n", i);
}
else
{
printf(
"Write failed for item[%d] with status %s\n", i,
UaStatus(results[i]).toString().toUtf8());
}
}
}
else
{
}
return result;
}
Finally, we call read from main:
...
{
printf("\nPress Enter to read values\n");
getchar();
status = pMyClient->read();
printf("\nPress Enter to write values\n");
getchar();
status = pMyClient->write();
printf("\nPress Enter to do a simple browse\n");
getchar();
...