UA Ansi C Server Professional  1.3.1.232
 All Data Structures Functions Variables Typedefs Enumerations Enumerator Groups Pages
Lesson 2: Extending the Address Space with Real World Data

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

Content:

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 of 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 The MachineType and the TemperatureSensorType

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 SDK Tutorial - How to create a new data provider for detailed information. In this example we create a new provider called CustomProvider.

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 startup of the server.

IFMETHODIMP(CustomProvider_Initialize)(UaServer_Provider* pProvider,
UaServer_pProviderCBInterface* pProviderCBInterface,
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 nodes of the provider. 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 UA Service Calls .

/* Initialization function called by the server */
IFMETHODIMP(CustomProvider_Initialize)(UaServer_Provider *a_pProvider,
UaServer_pProviderCBInterface *a_pProviderCBInterface,
UaServer_pProviderInterface *a_pProviderInterface)
{
OpcUa_InitializeStatus(OpcUa_Module_Server, "CustomProvider_Initialize");
printf("Initialize CustomProvider ...\n");
/* Store values */
g_pCustomProvider = a_pProvider;
g_pCustomProviderCBInterface = a_pProviderCBInterface;
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 must 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 numer of nodes that will be created is known at this point.

/* Register address space */
uStatus = a_pProviderCBInterface->RegisterAddressSpace((OpcUa_Handle *)a_pProvider,
&g_uCustomProvider_NamespaceIndex,
"http://customprovider/",
1000);
OpcUa_ReturnErrorIfBad(uStatus);

Finally the provider's nodes are created and the it's subscription management gets initialized:

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

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 startup sequence, right before UaServer_Providers_Initialize is being called:

UaServer_Provider customProvider;
...
/* Add custom provider */
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, so we get the OpcUa_BaseNode representing this node. After that we can create a new ObjectType node called TemperatureSensorType in the address space.

g_pCustomProviderCBInterface->AddressSpace_Get(0, &pServerAddressSpace);
/* 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.

Add instance declarations
For this 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

Now that the object types we want to use are created, we can instantiate them in the server. For that, 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 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 uses the Read service as an example, the other service calls are processed similarly.

Prepare the SDK to receive one 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 */
UaServer_Atomic_Increment(&a_pReadCtx->nOutstandingCbs);

Check if the request needs to be processed
The next step is to check if 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 if 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;
}