/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
* See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */
/**
* Working with Objects and Object Types
* -------------------------------------
*
* Using objects to structure information models
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* Assume a situation where we want to model a set of pumps and their runtime
* state in an OPC UA information model. Of course, all pump representations
* should follow the same basic structure, For example, we might have graphical
* representation of pumps in a SCADA visualisation that shall be resuable for
* all pumps.
*
* Following the object-oriented programming paradigm, every pump is represented
* by an object with the following layout:
*
* .. graphviz::
*
* digraph tree {
*
* fixedsize=true;
* node [width=2, height=0, shape=box, fillcolor="#E5E5E5", concentrate=true]
*
* node_root [label=< ObjectNode
Pump >]
*
* { rank=same
* point_1 [shape=point]
* node_1 [label=< VariableNode
ManufacturerName >] }
* node_root -> point_1 [arrowhead=none]
* point_1 -> node_1 [label="hasComponent"]
*
* { rank=same
* point_2 [shape=point]
* node_2 [label=< VariableNode
ModelName >] }
* point_1 -> point_2 [arrowhead=none]
* point_2 -> node_2 [label="hasComponent"]
*
* { rank=same
* point_4 [shape=point]
* node_4 [label=< VariableNode
Status >] }
* point_2 -> point_4 [arrowhead=none]
* point_4 -> node_4 [label="hasComponent"]
*
* { rank=same
* point_5 [shape=point]
* node_5 [label=< VariableNode
MotorRPM >] }
* point_4 -> point_5 [arrowhead=none]
* point_5 -> node_5 [label="hasComponent"]
*
* }
*
* The following code manually defines a pump and its member variables. We omit
* setting constraints on the variable values as this is not the focus of this
* tutorial and was already covered. */
#include
#include
#include
#include
static void
manuallyDefinePump(UA_Server *server) {
UA_NodeId pumpId; /* get the nodeid assigned by the server */
UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
oAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Pump (Manual)");
UA_Server_addObjectNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, "Pump (Manual)"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
oAttr, NULL, &pumpId);
UA_VariableAttributes mnAttr = UA_VariableAttributes_default;
UA_String manufacturerName = UA_STRING("Pump King Ltd.");
UA_Variant_setScalar(&mnAttr.value, &manufacturerName, &UA_TYPES[UA_TYPES_STRING]);
mnAttr.displayName = UA_LOCALIZEDTEXT("en-US", "ManufacturerName");
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "ManufacturerName"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), mnAttr, NULL, NULL);
UA_VariableAttributes modelAttr = UA_VariableAttributes_default;
UA_String modelName = UA_STRING("Mega Pump 3000");
UA_Variant_setScalar(&modelAttr.value, &modelName, &UA_TYPES[UA_TYPES_STRING]);
modelAttr.displayName = UA_LOCALIZEDTEXT("en-US", "ModelName");
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "ModelName"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), modelAttr, NULL, NULL);
UA_VariableAttributes statusAttr = UA_VariableAttributes_default;
UA_Boolean status = true;
UA_Variant_setScalar(&statusAttr.value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
statusAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Status");
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Status"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, NULL, NULL);
UA_VariableAttributes rpmAttr = UA_VariableAttributes_default;
UA_Double rpm = 50.0;
UA_Variant_setScalar(&rpmAttr.value, &rpm, &UA_TYPES[UA_TYPES_DOUBLE]);
rpmAttr.displayName = UA_LOCALIZEDTEXT("en-US", "MotorRPM");
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "MotorRPMs"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), rpmAttr, NULL, NULL);
}
/**
* Object types, type hierarchies and instantiation
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* Building up each object manually requires us to write a lot of code.
* Furthermore, there is no way for clients to detect that an object represents
* a pump. (We might use naming conventions or similar to detect pumps. But
* that's not exactly a clean solution.) Furthermore, we might have more devices
* than just pumps. And we require all devices to share some common structure.
* The solution is to define ObjectTypes in a hierarchy with inheritance
* relations.
*
* .. graphviz::
*
* digraph tree {
*
* fixedsize=true;
* node [width=2, height=0, shape=box, fillcolor="#E5E5E5", concentrate=true]
*
* node_root [label=< ObjectTypeNode
Device >]
*
* { rank=same
* point_1 [shape=point]
* node_1 [label=< VariableNode
ManufacturerName
(mandatory) >] }
* node_root -> point_1 [arrowhead=none]
* point_1 -> node_1 [label="hasComponent"]
*
* { rank=same
* point_2 [shape=point]
* node_2 [label=< VariableNode
ModelName >] }
* point_1 -> point_2 [arrowhead=none]
* point_2 -> node_2 [label="hasComponent"]
*
* { rank=same
* point_3 [shape=point]
* node_3 [label=< ObjectTypeNode
Pump >] }
* point_2 -> point_3 [arrowhead=none]
* point_3 -> node_3 [label="hasSubtype"]
*
* { rank=same
* point_4 [shape=point]
* node_4 [label=< VariableNode
Status
(mandatory) >] }
* node_3 -> point_4 [arrowhead=none]
* point_4 -> node_4 [label="hasComponent"]
*
* { rank=same
* point_5 [shape=point]
* node_5 [label=< VariableNode
MotorRPM >] }
* point_4 -> point_5 [arrowhead=none]
* point_5 -> node_5 [label="hasComponent"]
*
* }
*
* Children that are marked mandatory are automatically instantiated together
* with the parent object. This is indicated by a `hasModellingRule` reference
* to an object that representes the `mandatory` modelling rule. */
/* predefined identifier for later use */
UA_NodeId pumpTypeId = {1, UA_NODEIDTYPE_NUMERIC, {1001}};
static void
defineObjectTypes(UA_Server *server) {
/* Define the object type for "Device" */
UA_NodeId deviceTypeId; /* get the nodeid assigned by the server */
UA_ObjectTypeAttributes dtAttr = UA_ObjectTypeAttributes_default;
dtAttr.displayName = UA_LOCALIZEDTEXT("en-US", "DeviceType");
UA_Server_addObjectTypeNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
UA_QUALIFIEDNAME(1, "DeviceType"), dtAttr,
NULL, &deviceTypeId);
UA_VariableAttributes mnAttr = UA_VariableAttributes_default;
mnAttr.displayName = UA_LOCALIZEDTEXT("en-US", "ManufacturerName");
UA_NodeId manufacturerNameId;
UA_Server_addVariableNode(server, UA_NODEID_NULL, deviceTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "ManufacturerName"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), mnAttr, NULL, &manufacturerNameId);
/* Make the manufacturer name mandatory */
UA_Server_addReference(server, manufacturerNameId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);
UA_VariableAttributes modelAttr = UA_VariableAttributes_default;
modelAttr.displayName = UA_LOCALIZEDTEXT("en-US", "ModelName");
UA_Server_addVariableNode(server, UA_NODEID_NULL, deviceTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "ModelName"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), modelAttr, NULL, NULL);
/* Define the object type for "Pump" */
UA_ObjectTypeAttributes ptAttr = UA_ObjectTypeAttributes_default;
ptAttr.displayName = UA_LOCALIZEDTEXT("en-US", "PumpType");
UA_Server_addObjectTypeNode(server, pumpTypeId,
deviceTypeId, UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
UA_QUALIFIEDNAME(1, "PumpType"), ptAttr,
NULL, NULL);
UA_VariableAttributes statusAttr = UA_VariableAttributes_default;
statusAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Status");
statusAttr.valueRank = UA_VALUERANK_SCALAR;
UA_NodeId statusId;
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Status"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, NULL, &statusId);
/* Make the status variable mandatory */
UA_Server_addReference(server, statusId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);
UA_VariableAttributes rpmAttr = UA_VariableAttributes_default;
rpmAttr.displayName = UA_LOCALIZEDTEXT("en-US", "MotorRPM");
rpmAttr.valueRank = UA_VALUERANK_SCALAR;
UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "MotorRPMs"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), rpmAttr, NULL, NULL);
}
/**
* Now we add the derived ObjectType for the pump that inherits from the device
* object type. The resulting object contains all mandatory child variables.
* These are simply copied over from the object type. The object has a reference
* of type ``hasTypeDefinition`` to the object type, so that clients can detect
* the type-instance relation at runtime.
*/
static void
addPumpObjectInstance(UA_Server *server, char *name) {
UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
oAttr.displayName = UA_LOCALIZEDTEXT("en-US", name);
UA_Server_addObjectNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, name),
pumpTypeId, /* this refers to the object type
identifier */
oAttr, NULL, NULL);
}
/**
* Often times, we want to run a constructor function on a new object. This is
* especially useful when an object is instantiated at runtime (with the
* AddNodes service) and the integration with an underlying process canot be
* manually defined. In the following constructor example, we simply set the
* pump status to on.
*/
static UA_StatusCode
pumpTypeConstructor(UA_Server *server,
const UA_NodeId *sessionId, void *sessionContext,
const UA_NodeId *typeId, void *typeContext,
const UA_NodeId *nodeId, void **nodeContext) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "New pump created");
/* Find the NodeId of the status child variable */
UA_RelativePathElement rpe;
UA_RelativePathElement_init(&rpe);
rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
rpe.isInverse = false;
rpe.includeSubtypes = false;
rpe.targetName = UA_QUALIFIEDNAME(1, "Status");
UA_BrowsePath bp;
UA_BrowsePath_init(&bp);
bp.startingNode = *nodeId;
bp.relativePath.elementsSize = 1;
bp.relativePath.elements = &rpe;
UA_BrowsePathResult bpr =
UA_Server_translateBrowsePathToNodeIds(server, &bp);
if(bpr.statusCode != UA_STATUSCODE_GOOD ||
bpr.targetsSize < 1)
return bpr.statusCode;
/* Set the status value */
UA_Boolean status = true;
UA_Variant value;
UA_Variant_setScalar(&value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_Server_writeValue(server, bpr.targets[0].targetId.nodeId, value);
UA_BrowsePathResult_clear(&bpr);
/* At this point we could replace the node context .. */
return UA_STATUSCODE_GOOD;
}
static void
addPumpTypeConstructor(UA_Server *server) {
UA_NodeTypeLifecycle lifecycle;
lifecycle.constructor = pumpTypeConstructor;
lifecycle.destructor = NULL;
UA_Server_setNodeTypeLifecycle(server, pumpTypeId, lifecycle);
}
/** It follows the main server code, making use of the above definitions. */
UA_Boolean running = true;
static void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
int main(void) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_ServerConfig *config = UA_ServerConfig_new_default();
UA_Server *server = UA_Server_new(config);
manuallyDefinePump(server);
defineObjectTypes(server);
addPumpObjectInstance(server, "pump2");
addPumpObjectInstance(server, "pump3");
addPumpTypeConstructor(server);
addPumpObjectInstance(server, "pump4");
addPumpObjectInstance(server, "pump5");
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
UA_ServerConfig_delete(config);
return (int)retval;
}