ANSI C UA Server SDK  1.5.2.328
 All Data Structures Functions Variables Typedefs Enumerations Enumerator Modules Pages
Lesson 2: Extending the Address Space with Real World Data

This lesson explains how to create an OPC UA Server Address Space in order to describe a real world system with OPC UA.

Files used in this lesson:

Preliminary Note

The real world example used for this getting started lesson is an arbitrary machine that contains a heating element and a temperature sensor. Figure 2-1 shows the object types and their variables in the OPC UA notation.

This lesson only creates the object types and object instances with data variables and properties. Additional features like methods and events are added in the following lessons.

Figure 2-1 gives you an overview on the OPC UA object types MachineType and TemperatureSensorType. Both are directly derived from the BaseObjectType. The variable types of the different variable components are indicated in the figure.

Figure 2-1 MachineType and TemperatureSensorType

gettingstarted1_lesson02_machine_objecttypeall.png

Step 1: Creating a New Provider

Providers are responsible for managing a set of nodes for one OPC UA Namespace and for processing UA service calls concerning the provider’s nodes. See How to Create a New Data Provider for detailed information. In this example we create a new provider called CustomProvider. See custom_provider.c and custom_provider.h for the complete sample code.

Initialization Function

For integrating the provider into the server, all it needs to have is an initialization function of the type UaServer_pfInitializeProvider. This function is called by the SDK on server startup.

IFMETHODIMP(CustomProvider_Initialize)(UaServer_Provider *pProvider,
UaServer_pProviderInterface *pProviderInterface);

In this function we store the provided parameters for later use. The provider interface is filled with pointers to the functions that will handle the according service calls for the provider’s nodes. For that we use generic functions that check if the UA service call affects any of the provider’s nodes and call the SDK’s internal convenience functions to process the service call. Details about these handlers will follow in Step 5: Handling OPC UA Service Calls.

/* Initialization function called by the server */
IFMETHODIMP(CustomProvider_Initialize)(UaServer_Provider *a_pProvider,
UaServer_pProviderInterface *a_pProviderInterface)
{
OpcUa_InitializeStatus(OpcUa_Module_Server, "CustomProvider_Initialize");
printf("Initialize CustomProvider ...\n");
/* Store values */
g_pCustomProvider = a_pProvider;
g_pCustomProviderInterface = a_pProviderInterface;
OpcUa_MemSet(g_pCustomProviderInterface, 0, sizeof(UaServer_pProviderInterface));
/* Register service handlers */
a_pProviderInterface->Cleanup = CustomProvider_Cleanup;
a_pProviderInterface->ReadAsync = CustomProvider_ReadAsync;
a_pProviderInterface->WriteAsync = CustomProvider_WriteAsync;
a_pProviderInterface->BrowseAsync = CustomProvider_BrowseAsync;
a_pProviderInterface->TranslateAsync = CustomProvider_TranslateAsync;
a_pProviderInterface->AddItem = CustomProvider_AddItem;
a_pProviderInterface->RemoveItem = CustomProvider_RemoveItem;
a_pProviderInterface->Subscribe = CustomProvider_Subscribe;

Register Address Space and Initialize Provider

We also want to create some nodes, so we need to register an address space in the SDK. The namespace index returned by this function has to be used for our new nodes, so we also store it. The size of the address space is chosen very generous and might be reduced if the number of nodes that will be created is known at this point.

/* Register address space */
uStatus = UaServer_RegisterAddressSpace((OpcUa_Handle *)a_pProvider,
&g_uCustomProvider_NamespaceIndex,
"http://www.unifiedautomation.com/customprovider/",
1009);
OpcUa_ReturnErrorIfBad(uStatus);

Finally the provider’s nodes are created and its subscription management gets initialized:

/* Create address space */
uStatus = CustomProvider_CreateAddressSpace();
OpcUa_ReturnErrorIfBad(uStatus);
/* Initialize subscription management */
uStatus = CustomProvider_Subscription_Initialize();
OpcUa_ReturnErrorIfBad(uStatus);

Integrate the Provider into the Server

For adding the new provider to the server’s list of providers, we add following code into the main start up sequence, right before UaServer_Providers_Initialize is being called (see file servermain.c):

UaServer_Provider customProvider;
...
/* Add custom provider */
OpcUa_MemSet(&customProvider, 0, sizeof(customProvider));
customProvider.pfInit = CustomProvider_Initialize;
uStatus = UaServer_ProviderList_AddProvider(&uaServer, &customProvider);
OpcUa_GotoErrorIfBad(uStatus);

Step 2: Creating the TemperatureSensorType

For all of our nodes we use numeric NodeIds, starting with an identifier of 1. All node creation functions have a parameter a_pStartingNodeId that is incremented for each node, this way we can assure that the NodeIds used are unique in our namespace.

For creating the type and instance nodes the example provides some convenience functions. The first ones, CustomProvider_CreateMachineType and CustomProvider_CreateTemperatureSensorType, create all needed object types in the types tree of the server.

Create Type Nodes

For creating a new node, we need to pass the parent node as a parameter to the create function. In our case the new node will be a child of the BaseObjectType in the server address space (see Figure 2-1), so we get the OpcUa_BaseNode representing this node. After that we can create a new ObjectType node called TemperatureSensorType in the address space.

UaServer_AddressSpace_Get(0, &pServerAddressSpace);
OpcUa_NodeId_Initialize(&nodeId);
OpcUa_NodeId_Initialize(&referenceNodeId);
/* Create object type */
nodeId.NamespaceIndex = 0;
nodeId.Identifier.Numeric = OpcUaId_BaseObjectType;
UaServer_GetNode(pServerAddressSpace, &nodeId, &pBaseNode);
a_pStartingNodeId->Identifier.Numeric++;
uStatus = UaServer_CreateObjectType(pAddressSpace,
&pNewSensorType,
pBaseNode,
a_pStartingNodeId->Identifier.Numeric,
g_uCustomProvider_NamespaceIndex,
"TemperatureSensorType");
OpcUa_GotoErrorIfBad(uStatus);

Append Properties

Our new object type should have a property Temperature which will represent the sensor’s temperature. This property is created as a child of the previously created type and a default value is assigned.

/* Create Temperature property */
a_pStartingNodeId->Identifier.Numeric++;
uStatus = UaServer_CreateDataVariable(pAddressSpace,
&pVariable,
(OpcUa_BaseNode*)pNewSensorType,
a_pStartingNodeId->Identifier.Numeric,
g_uCustomProvider_NamespaceIndex,
"Temperature");
OpcUa_GotoErrorIfBad(uStatus);
OpcUa_Variable_SetDataType_Numeric(pVariable, OpcUaId_Double, 0);
pValue = OpcUa_Variable_GetValue(pVariable);
pValue->Datatype = OpcUaType_Double;
pValue->Value.Double = 25.0;

Add Modelling Rules

Finally, a modelling rule reference is created between the type and the property to define that every instance of the type needs to have this property.

/* Set ModellingRule Mandatory for Temperature property */
nodeId.NamespaceIndex = 0;
nodeId.Identifier.Numeric = OpcUaId_ModellingRule_Mandatory;
referenceNodeId.Identifier.Numeric = OpcUaId_HasModellingRule;
uStatus = OpcUa_BaseNode_AddReferenceToNodeId(pVariable, &nodeId, &referenceNodeId);
OpcUa_GotoErrorIfBad(uStatus);

Step 3: Creating the MachineType

Creating the MachineType follows the same steps as Step 2: Creating the TemperatureSensorType, with one exception: all machines should contain a TemperatureSensor object (see Figure 2-1).

Add Instance Declarations

For this purpose, we need to create a TemperatureSensor instance as child of the MachineType and mark it as mandatory.

OpcUa_StatusCode CustomProvider_CreateMachineType(OpcUa_NodeId *a_pStartingNodeId,
OpcUa_ObjectType **a_ppMachineType,
OpcUa_ObjectType **a_ppTemperatureSensorType)
{
...
/* Create TemperatureSensor child object */
referenceNodeId.Identifier.Numeric = OpcUaId_HasComponent;
a_pStartingNodeId->Identifier.Numeric++;
uStatus = UaServer_CreateNode(pAddressSpace,
(OpcUa_BaseNode **)&pTemperatureSensor,
(OpcUa_BaseNode*)pNewMachineType,
a_pStartingNodeId,
OpcUa_NodeClass_Object,
&referenceNodeId,
OpcUa_BaseNode_GetId(*a_ppTemperatureSensorType),
"TemperatureSensor",
"TemperatureSensor",
"TemperatureSensor");
OpcUa_GotoErrorIfBad(uStatus);
/* Set ModellingRule Mandatory for TemperatureSensor child object */
nodeId.NamespaceIndex = 0;
nodeId.Identifier.Numeric = OpcUaId_ModellingRule_Mandatory;
referenceNodeId.Identifier.Numeric = OpcUaId_HasModellingRule;
uStatus = OpcUa_BaseNode_AddReferenceToNodeId(pTemperatureSensor, &nodeId, &referenceNodeId);
OpcUa_GotoErrorIfBad(uStatus);
/* Create TemperatureSensor's Temperature property */
a_pStartingNodeId->Identifier.Numeric++;
uStatus = UaServer_CreateDataVariable(pAddressSpace,
&pVariable,
(OpcUa_BaseNode*)pTemperatureSensor,
a_pStartingNodeId->Identifier.Numeric,
g_uCustomProvider_NamespaceIndex,
"Temperature");
OpcUa_GotoErrorIfBad(uStatus);
OpcUa_Variable_SetDataType_Numeric(pVariable, OpcUaId_Double, 0);
pValue = OpcUa_Variable_GetValue(pVariable);
pValue->Datatype = OpcUaType_Double;
pValue->Value.Double = 25.0;
/* Set ModellingRule Mandatory for TemperatureSensor's Temperature property */
nodeId.NamespaceIndex = 0;
nodeId.Identifier.Numeric = OpcUaId_ModellingRule_Mandatory;
referenceNodeId.Identifier.Numeric = OpcUaId_HasModellingRule;
uStatus = OpcUa_BaseNode_AddReferenceToNodeId(pVariable, &nodeId, &referenceNodeId);
OpcUa_GotoErrorIfBad(uStatus);

Step 4: Instantiating a Machine Object

After having created the object types, we can instantiate them in the server. For this purpose, the convenience function CustomProvider_CreateMachine is used, passing the names of the nodes to create, the NodeIds of the object types we created in the previous steps, and a pointer to the node which should be the parent of the newly created machine.

The new machine instance is placed in a folder “Custom provider” that is created for grouping all nodes of our provider.

/* Create custom provider base node */
nodeId.Identifier.Numeric++;
uStatus = UaServer_CreateFolder(pAddressSpace,
&pCustomProvider,
(OpcUa_BaseNode*)pFolder,
nodeId.Identifier.Numeric,
uNsIdx,
"Custom Provider");
OpcUa_GotoErrorIfBad(uStatus);
/* Create instance of MachineType */
uStatus = CustomProvider_CreateMachine("MyCustomMachine",
"MyCustomTemperatureSensor",
(OpcUa_BaseNode*)pCustomProvider,
OpcUa_BaseNode_GetId(pMachineType),
OpcUa_BaseNode_GetId(pTemperatureSensorType),
&nodeId);
OpcUa_GotoErrorIfBad(uStatus);

Step 5: Handling OPC UA Service Calls

The UA service call handlers of the provider are generic functions that work on the OpcUa_BaseNode nodes managed by the SDK. For providers using these nodes, the SDK provides helper functions for processing the service calls. The following code from custom_provider_read.c uses the Read service as an example, the other service calls are processed similarly (see custom_provider_browse.c, custom_provider_subscription.c, and custom_provider_write.c).

Prepare the SDK to Receive a Callback

The UA service handlers of a provider can process the calls asynchronously, this is why every call context has a member nOutstandingCbs. The provider increments this value by the number of callbacks it will send before processing the request and decrements the value when it has finished. In our case one callback will be sent as the processing is done synchronously, so we increment the value by one.

IFMETHODIMP(CustomProvider_ReadAsync)(UaServer_ProviderReadContext *a_pReadCtx)
{
...
/* We will send exactly one callback */
UaBase_Atomic_Increment(&a_pReadCtx->nOutstandingCbs);

Check Whether the Request Needs to Be Processed

The next step is to check whether we are responsible for processing the request. This is the case if the namespace index of a node is the namespace index of our provider. If this is true, we retrieve the OpcUa_BaseNode represented by the NodeId for working with it.

/* Iterate over nodes and check if the provider is responsible for processing them */
for (i = 0; i < pReq->NoOfNodesToRead; i++)
{
if (pReq->NodesToRead[i].NodeId.NamespaceIndex == g_uCustomProvider_NamespaceIndex)
{
pNodeId = &(pReq->NodesToRead[i].NodeId);
UaServer_GetNode(pAddressSpace, pNodeId, &pNode);
if (pNode)
{

Test Access Rights and Process the Request

Now we can check whether the node is readable by checking the AccessLevel of it. If this test is passed, we call the SDK’s helper function UaServer_ReadInternal to process the service call for us.

if ( (a_pReadCtx->pRequest->NodesToRead[i].AttributeId == OpcUa_Attributes_Value &&
OpcUa_BaseNode_GetType(pNode) == eVariable &&
(OpcUa_Variable_GetAccessLevel(pNode) & OpcUa_AccessLevels_CurrentRead) == 0) )
{
pRes->Results[i].StatusCode = OpcUa_BadNotReadable;
continue;
}
/* Use the SDK's helper function to process the request */
UaServer_ReadInternal(pNode, a_pReadCtx, i);

Send Callback

Now we must call the callback function of the SDK to inform it that we are finished with processing the call. After all expected callbacks have been sent to the SDK, it will trigger sending the response to the client.

/* Send callback */
UaServer_ReadComplete(a_pReadCtx);
OpcUa_ReturnStatusCode;
OpcUa_BeginErrorHandling;
OpcUa_FinishErrorHandling;
}

Step 6: Run Application

Compile and run the server application. When connecting to the server with a UA Client (e.g. UaExpert) we can observe that the newly created types and objects are visible in the server’s address space.

Figure 2-2 Types and Objects in the Server’s Address Space

gettingstarted1_lesson02_types_and_objects.png