C++ Based OPC UA Client/Server/PubSub SDK  1.7.6.537
Lesson 3: Browse, Read Configuration, and Write Values

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

...
UaStatus connect();
UaStatus disconnect();
UaStatus browseSimple(); // Add this line
UaStatus browseContinuationPoint(); // Add this line
UaStatus read();
UaStatus subscribe();
UaStatus unsubscribe();
// New code begins
private:
// helper methods
UaStatus browseInternal(const UaNodeId& nodeToBrowse, OpcUa_UInt32 maxReferencesToReturn);
void printBrowseResults(const UaReferenceDescriptions& referenceDescriptions);
//New code ends
...

Add the following code snippets to sampleclient.cpp

UaStatus SampleClient::browseSimple()
{
UaStatus result;
UaNodeId nodeToBrowse;
// browse from root folder with no limitation of references to return
nodeToBrowse = UaNodeId(OpcUaId_RootFolder);
result = browseInternal(nodeToBrowse, 0);
return result;
}
UaStatus SampleClient::browseContinuationPoint()
{
UaStatus result;
UaNodeId nodeToBrowse;
// browse from Massfolder with max references to return set to 5
nodeToBrowse = UaNodeId("Demo", 2);
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)
{
UaStatus result;
ServiceSettings serviceSettings;
BrowseContext browseContext;
UaByteString continuationPoint;
UaReferenceDescriptions referenceDescriptions;
// configure browseContext
browseContext.browseDirection = OpcUa_BrowseDirection_Forward;
browseContext.referenceTypeId = OpcUaId_HierarchicalReferences;
browseContext.includeSubtype = OpcUa_True;
browseContext.maxReferencesToReturn = maxReferencesToReturn;
printf("\nBrowsing from Node %s...\n", nodeToBrowse.toXmlString().toUtf8());
result = m_pSession->browse(
serviceSettings,
nodeToBrowse,
browseContext,
continuationPoint,
referenceDescriptions);
if (result.isGood())
{
// print results
printBrowseResults(referenceDescriptions);
// continue browsing
while (continuationPoint.length() > 0)
{
printf("\nContinuationPoint is set. BrowseNext...\n");
// browse next
result = m_pSession->browseNext(
serviceSettings,
OpcUa_False,
continuationPoint,
referenceDescriptions);
if (result.isGood())
{
// print results
printBrowseResults(referenceDescriptions);
}
else
{
// Service call failed
printf("BrowseNext failed with status %s\n", result.toString().toUtf8());
}
}
}
else
{
// Service call failed
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:

void SampleClient::printBrowseResults(const UaReferenceDescriptions& referenceDescriptions)
{
OpcUa_UInt32 i;
for (i=0; i<referenceDescriptions.length(); i++)
{
printf("node: ");
UaNodeId referenceTypeId(referenceDescriptions[i].ReferenceTypeId);
printf("[Ref=%s] ", referenceTypeId.toString().toUtf8() );
UaQualifiedName browseName(referenceDescriptions[i].BrowseName);
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

// Connect succeeded
if (status.isGood())
{
// Read values
status = pMyClient->read();
// Wait for user command.
printf("\nPress Enter to create subscription\n");
printf("Press Enter again to stop subscription\n");
getchar();
// Create subscription
status = pMyClient->subscribe();
if (status.isGood())
{
// Wait for user command.
getchar();
// Delete subscription
status = pMyClient->unsubscribe();
}
// Wait for user command.
printf("\nPress Enter to disconnect\n");
getchar();
// Disconnect from OPC UA Server
status = pMyClient->disconnect();
}

with

// Connect succeeded
if (status.isGood())
{
// Wait for user command.
printf("\nPress Enter to do a simple browse\n");
getchar();
// Simple Browse
status = pMyClient->browseSimple();
// Wait for user command.
printf("\nPress Enter to browse with continuation point\n");
getchar();
// Browse with continuation point
status = pMyClient->browseContinuationPoint();
// Wait for user command.
printf("\nPress Enter to disconnect\n");
getchar();
// Disconnect from OPC UA Server
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();
// get connection and session parameters
UaString getServerUrl() const;
UaString getDiscoveryUrl() const;
UaString getApplicationName() const;
UaString getProductUri() const;
OpcUa_Boolean getAutomaticReconnect() const;
OpcUa_Boolean getRetryInitialConnect() const;
// load configuration from file to fill connection parameters, NamespaceArray and NodeIds
UaStatus loadConfiguration(const UaString& sConfigurationFile);
private:
// connection and session configuration
UaString m_applicationName;
UaString m_serverUrl;
UaString m_discoveryUrl;
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.

UaStatus Configuration::loadConfiguration(const UaString& sConfigurationFile)
{
UaStatus result;
UaVariant value;
UaSettings* pSettings = NULL;
pSettings = new UaSettings(sConfigurationFile.toUtf16());
pSettings->beginGroup("UaSampleConfig");
// Application Name
value = pSettings->value("ApplicationName", UaString());
m_applicationName = value.toString();
// Server URLs
value = pSettings->value("DiscoveryURL", UaString("opc.tcp://localhost:48010"));
m_discoveryUrl = value.toString();
value = pSettings->value("ServerUrl", UaString("opc.tcp://localhost:48010"));
m_serverUrl = value.toString();
// Reconnection settings
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);
pSettings->endGroup(); // UaClientConfig
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" // Add this line
SampleClient::SampleClient()
{
m_pSession = new UaSession();
m_pSampleSubscription = new SampleSubscription();
m_pConfiguration = new Configuration(); // Add this line
}
SampleClient::~SampleClient()
{
if (m_pSampleSubscription)
{
...
}
if (m_pSession)
{
...
}
// New code begins
if (m_pConfiguration)
{
delete m_pConfiguration;
m_pConfiguration = NULL;
}
// New code ends
}

Add the following code to sampleclient.h

#ifndef SAMPLECLIENT_H
#define SAMPLECLIENT_H
#include "uabase.h"
#include "uaclientsdk.h"
class SampleSubscription;
class Configuration; // Add this line
using namespace UaClientSdk
class SampleClient : public UaSessionCallback
{
UA_DISABLE_COPY(SampleClient);
public:
SampleClient();
virtual ~SampleClient();
// UaSessionCallback implementation ----------------------------------------------------
virtual void connectionStatusChanged(OpcUa_UInt32 clientConnectionId, UaClient::ServerStatus serverStatus);
// UaSessionCallback implementation ------------------------------------------------------
// New code begins
// set a configuration object we use to get connection parameters and NodeIds
void setConfiguration(Configuration* pConfiguration);
// New code ends
...
private:
UaSession* m_pSession;
SampleSubscription* m_pSampleSubscription;
Configuration* m_pConfiguration; // Add this line

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.

UaStatus SampleClient::connect()
{
UaStatus result;
// Provide information about the client
SessionConnectInfo sessionConnectInfo;
UaString sNodeName("unknown_host");
char szHostName[256];
if (0 == UA_GetHostname(szHostName, 256))
{
sNodeName = szHostName;
}
// Fill session connect info with data from configuration
sessionConnectInfo.sApplicationName = m_pConfiguration->getApplicationName();
// Use the host name to generate a unique application URI
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();
// Security settings are not initialized - we connect without security for now
SessionSecurityInfo sessionSecurityInfo;
printf("\nConnecting to %s\n", m_pConfiguration->getServerUrl().toUtf8());
result = m_pSession->connect(
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:

  • shutdown.h
  • shutdown.cpp

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" // Add this line
#include "shutdown.h" // Add this line
...
// Create instance of SampleClient
SampleClient* pMyClient = new SampleClient();
// New code begins
// get current path to build configuration file name
UaString sConfigFile(getAppPath());
sConfigFile += "/sampleconfig.ini";
// Create configuration object and load configuration
Configuration* pMyConfiguration = new Configuration();
status = pMyConfiguration->loadConfiguration(sConfigFile);
// Loading configuration succeeded
if (status.isGood())
{
// set configuration
pMyClient->setConfiguration(pMyConfiguration);
// New code ends
// Connect to OPC UA Server
status = pMyClient->connect();
// New code begins
}
else
{
delete pMyConfiguration;
pMyConfiguration = NULL;
}
// New code ends

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

...
// load configuration from file to fill connection parameters, NamespaceArray and NodeIds
UaStatus loadConfiguration(const UaString& sConfigurationFile);
// New code begins
// update the namespace indexes for all nodeIds and update the internal namespaceArray
UaStatus updateNamespaceIndexes(const UaStringArray& namespaceArray);
// New code ends
private:
// connection and session configuration
UaString m_applicationName;
UaString m_discoveryUrl;
OpcUa_Boolean m_bAutomaticReconnect;
OpcUa_Boolean m_bRetryInitialConnect;
// New code begins
// NamespaceArray and NodeIds
UaStringArray m_namespaceArray;
// New code ends
};

Add the following code to loadConfiguration to read the namespace array from the configuration file (documentation for UaSettings).

UaStatus Configuration::loadConfiguration(const UaString& sConfigurationFile)
{
UaStatus result;
UaVariant value;
// New code begins
UaString sTempKey;
OpcUa_UInt32 i = 0;
OpcUa_UInt32 size = 0;
// New code ends
UaSettings* pSettings = NULL;
pSettings = new UaSettings(sConfigurationFile.toUtf16());
...
// Reconnection settings
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);
// New code begins
// Read NamespaceArray
m_namespaceArray.clear();
pSettings->beginGroup("NSArray");
value = pSettings->value("size", (OpcUa_UInt32)0);
value.toUInt32(size);
m_namespaceArray.resize(size);
for (i=0; i<size;i++)
{
sTempKey = UaString("NameSpaceUri0%1").arg((int)i);
value = pSettings->value(sTempKey.toUtf16(), UaString(""));
value.toString().copyTo(&m_namespaceArray[i]);
}
pSettings->endGroup(); // NSArray
// New code ends
pSettings->endGroup(); // UaClientConfig
delete pSettings;
pSettings = NULL;
return result;
}

Add the method updateNamespaceIndexes to configuration.cpp:

UaStatus Configuration::updateNamespaceIndexes(const UaStringArray& namespaceArray)
{
UaStatus result;
OpcUa_UInt32 i, j;
OpcUa_UInt32 size;
// create mapping table
size = m_namespaceArray.length();
UaInt16Array mappingTable;
mappingTable.resize(size);
// fill mapping table
for (i = 0; i < m_namespaceArray.length(); i++)
{
mappingTable[i] = (OpcUa_UInt16)i;
// find namespace uri
for (j = 0; j < namespaceArray.length(); j++)
{
UaString string1(m_namespaceArray[i]);
UaString string2(namespaceArray[j]);
if (string1 == string2)
{
mappingTable[i] = (OpcUa_UInt16)j;
break;
}
}
}
// update namespace array
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:
UaSession* m_pSession;
SampleSubscription* m_pSampleSubscription;
Configuration* m_pConfiguration;
UaClient::ServerStatus m_serverStatus; // Add this line
};
#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");
// New code begins
if (m_serverStatus != UaClient::NewSessionCreated)
{
m_pConfiguration->updateNamespaceIndexes(m_pSession->getNamespaceTable());
}
// New code ends
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");
// New code begins
m_pConfiguration->updateNamespaceIndexes(m_pSession->getNamespaceTable());
// New code ends
break;
}
printf("-------------------------------------------------------------\n");
m_serverStatus = serverStatus; // Add this line
}

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

...
// get connection and session parameters
UaString getServerUrl() const;
UaString getDiscoveryUrl() const;
UaString getApplicationName() const;
UaString getProductUri() const;
OpcUa_Boolean getAutomaticReconnect() const;
OpcUa_Boolean getRetryInitialConnect() const;
// New code begins
// get lists of NodeIds and values
UaNodeIdArray getNodesToRead() const;
// New code ends
...
private:
...
// NamespaceArray and NodeIds
UaStringArray m_namespaceArray;
UaNodeIdArray m_nodesToRead; // Add this line
};
#endif // CONFIGURATION_H

Implement the method getNodesToRead in configuration.cpp:

UaNodeIdArray Configuration::getNodesToRead() const
{
return m_nodesToRead;
}

The method Configuration::loadConfiguration has to be extended to read the NodeIds from the configuration file (documentation for UaSettings).

...
// Read NamespaceArray
m_namespaceArray.clear();
pSettings->beginGroup("NSArray");
value = pSettings->value("size", (OpcUa_UInt32)0);
value.toUInt32(size);
m_namespaceArray.resize(size);
for (i=0; i<size;i++)
{
sTempKey = UaString("NameSpaceUri0%1").arg((int)i);
value = pSettings->value(sTempKey.toUtf16(), UaString(""));
value.toString().copyTo(&m_namespaceArray[i]);
}
pSettings->endGroup(); // NSArray
// New code begins
// Read NodeIds to use for reading
m_nodesToRead.clear();
pSettings->beginGroup("NodesToRead");
value = pSettings->value("size", (OpcUa_UInt32)0);
value.toUInt32(size);
m_nodesToRead.resize(size);
for (i=0; i<size;i++)
{
sTempKey = UaString("Variable0%1").arg((int)i);
value = pSettings->value(sTempKey.toUtf16(), UaString(""));
UaNodeId::fromXmlString(value.toString()).copyTo(&m_nodesToRead[i]);
}
pSettings->endGroup(); // NodesToRead
// New code ends
...

Add the following code to Configuration::updateNamespaceIndexes to replace the namespace index in NodeIds.

...
// New code begins
// replace namespace index in NodeIds
// NodesToRead
for (i = 0; i < m_nodesToRead.length(); i++)
{
m_nodesToRead[i].NamespaceIndex = mappingTable[m_nodesToRead[i].NamespaceIndex];
}
// New code ends
// update namespace array
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:

UaStatus SampleClient::read()
{
UaStatus result;
ServiceSettings serviceSettings;
UaReadValueIds nodesToRead;
UaDataValues values;
UaDiagnosticInfos diagnosticInfos;
// read all nodes from the configuration
UaNodeIdArray nodes = m_pConfiguration->getNodesToRead();
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);
if (result.isGood())
{
// Read service succeded - check individual status codes
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
{
// Service call failed
printf("Read failed with status %s\n", result.toString().toUtf8());
}
return result;
}

Now we are ready to call read from main:

...
// Connect succeeded
if (status.isGood())
{
// New code begins
// Wait for user command.
printf("\nPress Enter to read values\n");
getchar();
// Read values
status = pMyClient->read();
// New code ends
...

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

...
// get lists of NodeIds and values
UaNodeIdArray getNodesToRead() const;
// New code begins
UaNodeIdArray getNodesToWrite() const;
UaVariantArray getWriteValues() const;
// New code ends
...
private:
...
// NamespaceArray and NodeIds
UaStringArray m_namespaceArray;
UaNodeIdArray m_nodesToRead;
// New code begins
UaNodeIdArray m_nodesToWrite;
UaVariantArray m_writeValues;
// New code ends
};

Implement the methods getNodesToWrite and getWriteValues in configuration.cpp.

UaNodeIdArray Configuration::getNodesToWrite() const
{
return m_nodesToWrite;
}
UaVariantArray Configuration::getWriteValues() const
{
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.

UaStatus Configuration::loadConfiguration(const UaString& sConfigurationFile)
{
UaStatus result;
UaVariant value;
UaString sTempKey;
OpcUa_UInt32 i = 0;
OpcUa_UInt32 size = 0;
OpcUa_Byte byte; // Add this line
UaSettings* pSettings = NULL;
pSettings = new UaSettings(sConfigurationFile.toUtf16());
...
// Read NodeIds to use for reading
m_nodesToRead.clear();
pSettings->beginGroup("NodesToRead");
value = pSettings->value("size", (OpcUa_UInt32)0);
value.toUInt32(size);
m_nodesToRead.resize(size);
for (i=0; i<size;i++)
{
sTempKey = UaString("Variable0%1").arg((int)i);
value = pSettings->value(sTempKey.toUtf16(), UaString(""));
UaNodeId::fromXmlString(value.toString()).copyTo(&m_nodesToRead[i]);
}
pSettings->endGroup(); // NodesToRead
// New code begins
// Read NodeIds, DataTypes and Values to use for writing
m_nodesToWrite.clear();
pSettings->beginGroup("NodesToWrite");
value = pSettings->value("size", (OpcUa_UInt32)0);
value.toUInt32(size);
// NodeIds
m_nodesToWrite.resize(size);
for (i=0; i<size;i++)
{
sTempKey = UaString("Variable0%1").arg((int)i);
value = pSettings->value(sTempKey.toUtf16(), UaString(""));
UaNodeId::fromXmlString(value.toString()).copyTo(&m_nodesToWrite[i]);
}
// DataTypes
UaByteArray writeDataTypes;
writeDataTypes.resize(size);
for (i=0; i<size;i++)
{
sTempKey = UaString("DataType0%1").arg((int)i);
value = pSettings->value(sTempKey.toUtf16(), UaString(""));
value.toByte(byte);
writeDataTypes[(int)i] = byte;
}
// Values
m_writeValues.resize(size);
for (i=0; i<size;i++)
{
sTempKey = UaString("Value0%1").arg((int)i);
value = pSettings->value(sTempKey.toUtf16());
// convert the value to the correct type
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());
}
}
pSettings->endGroup(); // NodesToWrite
// New code ends

Configuration::updateNamespaceIndexes has to be extended to replace the namespace index in NodeIds of the nodes to write.

...
// replace namespace index in NodeIds
// NodesToRead
for (i = 0; i < m_nodesToRead.length(); i++)
{
m_nodesToRead[i].NamespaceIndex = mappingTable[m_nodesToRead[i].NamespaceIndex];
}
// New code begins
// NodeToWrite
for (i = 0; i < m_nodesToWrite.length(); i++)
{
m_nodesToWrite[i].NamespaceIndex = mappingTable[m_nodesToWrite[i].NamespaceIndex];
}
// New code ends
...

We need to implement the method write which uses UaSession::write to write the values.

Add method write to sampleclient.h

...
UaStatus connect();
UaStatus disconnect();
UaStatus browseSimple();
UaStatus browseContinuationPoint();
UaStatus read();
UaStatus write(); // Add this line
UaStatus subscribe();
UaStatus unsubscribe();
...

Then we implement write

UaStatus SampleClient::write()
{
UaStatus result;
ServiceSettings serviceSettings;
UaWriteValues nodesToWrite;
UaDiagnosticInfos diagnosticInfos;
// write all nodes from the configuration
UaNodeIdArray nodes = m_pConfiguration->getNodesToWrite();
UaVariantArray values = m_pConfiguration->getWriteValues();
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);
// set value to write
OpcUa_Variant_CopyTo(&values[i], &nodesToWrite[i].Value.Value);
}
printf("\nWriting...\n");
result = m_pSession->write(
serviceSettings,
nodesToWrite,
results,
diagnosticInfos);
if (result.isGood())
{
// Write service succeded - check individual status codes
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
{
// Service call failed
printf("Write failed with status %s\n", result.toString().toUtf8());
}
return result;
}

Finally, we call read from main:

...
// Connect succeeded
if (status.isGood())
{
// Wait for user command.
printf("\nPress Enter to read values\n");
getchar();
// Read values
status = pMyClient->read();
// New code begins
// Wait for user command.
printf("\nPress Enter to write values\n");
getchar();
// Write values
status = pMyClient->write();
// New code ends
// Wait for user command.
printf("\nPress Enter to do a simple browse\n");
getchar();
...