|
@@ -0,0 +1,336 @@
|
|
|
+/* 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=< <I>ObjectNode</I><BR/>Pump >]
|
|
|
+ *
|
|
|
+ * { rank=same
|
|
|
+ * point_1 [shape=point]
|
|
|
+ * node_1 [label=< <I>VariableNode</I><BR/>ManufacturerName >] }
|
|
|
+ * node_root -> point_1 [arrowhead=none]
|
|
|
+ * point_1 -> node_1 [label="hasComponent"]
|
|
|
+ *
|
|
|
+ * { rank=same
|
|
|
+ * point_2 [shape=point]
|
|
|
+ * node_2 [label=< <I>VariableNode</I><BR/>ModelName >] }
|
|
|
+ * point_1 -> point_2 [arrowhead=none]
|
|
|
+ * point_2 -> node_2 [label="hasComponent"]
|
|
|
+ *
|
|
|
+ * { rank=same
|
|
|
+ * point_4 [shape=point]
|
|
|
+ * node_4 [label=< <I>VariableNode</I><BR/>Status >] }
|
|
|
+ * point_2 -> point_4 [arrowhead=none]
|
|
|
+ * point_4 -> node_4 [label="hasComponent"]
|
|
|
+ *
|
|
|
+ * { rank=same
|
|
|
+ * point_5 [shape=point]
|
|
|
+ * node_5 [label=< <I>VariableNode</I><BR/>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 <signal.h>
|
|
|
+#include "open62541.h"
|
|
|
+
|
|
|
+static void
|
|
|
+manuallyDefinePump(UA_Server *server) {
|
|
|
+ UA_NodeId pumpId; /* get the nodeid assigned by the server */
|
|
|
+ UA_ObjectAttributes oAttr;
|
|
|
+ UA_ObjectAttributes_init(&oAttr);
|
|
|
+ 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_NULL,
|
|
|
+ oAttr, NULL, &pumpId);
|
|
|
+
|
|
|
+ UA_VariableAttributes mnAttr;
|
|
|
+ UA_VariableAttributes_init(&mnAttr);
|
|
|
+ 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_NULL, mnAttr, NULL, NULL);
|
|
|
+
|
|
|
+ UA_VariableAttributes modelAttr;
|
|
|
+ UA_VariableAttributes_init(&modelAttr);
|
|
|
+ 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_NULL, modelAttr, NULL, NULL);
|
|
|
+
|
|
|
+ UA_VariableAttributes statusAttr;
|
|
|
+ UA_VariableAttributes_init(&statusAttr);
|
|
|
+ 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_NULL, statusAttr, NULL, NULL);
|
|
|
+
|
|
|
+ UA_VariableAttributes rpmAttr;
|
|
|
+ UA_VariableAttributes_init(&rpmAttr);
|
|
|
+ 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_NULL, 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=< <I>ObjectTypeNode</I><BR/>Device >]
|
|
|
+ *
|
|
|
+ * { rank=same
|
|
|
+ * point_1 [shape=point]
|
|
|
+ * node_1 [label=< <I>VariableNode</I><BR/>ManufacturerName >] }
|
|
|
+ * node_root -> point_1 [arrowhead=none]
|
|
|
+ * point_1 -> node_1 [label="hasComponent"]
|
|
|
+ *
|
|
|
+ * { rank=same
|
|
|
+ * point_2 [shape=point]
|
|
|
+ * node_2 [label=< <I>VariableNode</I><BR/>ModelName >] }
|
|
|
+ * point_1 -> point_2 [arrowhead=none]
|
|
|
+ * point_2 -> node_2 [label="hasComponent"]
|
|
|
+ *
|
|
|
+ * { rank=same
|
|
|
+ * point_3 [shape=point]
|
|
|
+ * node_3 [label=< <I>ObjectTypeNode</I><BR/>Pump >] }
|
|
|
+ * point_2 -> point_3 [arrowhead=none]
|
|
|
+ * point_3 -> node_3 [label="hasSubtype"]
|
|
|
+ *
|
|
|
+ * { rank=same
|
|
|
+ * point_4 [shape=point]
|
|
|
+ * node_4 [label=< <I>VariableNode</I><BR/>Status >] }
|
|
|
+ * node_3 -> point_4 [arrowhead=none]
|
|
|
+ * point_4 -> node_4 [label="hasComponent"]
|
|
|
+ *
|
|
|
+ * { rank=same
|
|
|
+ * point_5 [shape=point]
|
|
|
+ * node_5 [label=< <I>VariableNode</I><BR/>MotorRPM >] }
|
|
|
+ * point_4 -> point_5 [arrowhead=none]
|
|
|
+ * point_5 -> node_5 [label="hasComponent"]
|
|
|
+ *
|
|
|
+ * }
|
|
|
+ *
|
|
|
+ */
|
|
|
+
|
|
|
+/* 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_init(&dtAttr);
|
|
|
+ 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_init(&mnAttr);
|
|
|
+ mnAttr.displayName = UA_LOCALIZEDTEXT("en_US", "ManufacturerName");
|
|
|
+ UA_Server_addVariableNode(server, UA_NODEID_NULL, deviceTypeId,
|
|
|
+ UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
|
|
|
+ UA_QUALIFIEDNAME(1, "ManufacturerName"),
|
|
|
+ UA_NODEID_NULL, mnAttr, NULL, NULL);
|
|
|
+
|
|
|
+ UA_VariableAttributes modelAttr;
|
|
|
+ UA_VariableAttributes_init(&modelAttr);
|
|
|
+ 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_NULL, modelAttr, NULL, NULL);
|
|
|
+
|
|
|
+ /* Define the object type for "Pump" */
|
|
|
+ UA_ObjectTypeAttributes ptAttr;
|
|
|
+ UA_ObjectTypeAttributes_init(&ptAttr);
|
|
|
+ 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_init(&statusAttr);
|
|
|
+ statusAttr.displayName = UA_LOCALIZEDTEXT("en_US", "Status");
|
|
|
+ statusAttr.valueRank = -1;
|
|
|
+ UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpTypeId,
|
|
|
+ UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
|
|
|
+ UA_QUALIFIEDNAME(1, "Status"),
|
|
|
+ UA_NODEID_NULL, statusAttr, NULL, NULL);
|
|
|
+
|
|
|
+ UA_VariableAttributes rpmAttr;
|
|
|
+ UA_VariableAttributes_init(&rpmAttr);
|
|
|
+ rpmAttr.displayName = UA_LOCALIZEDTEXT("en_US", "MotorRPM");
|
|
|
+ rpmAttr.valueRank = -1;
|
|
|
+ UA_Server_addVariableNode(server, UA_NODEID_NULL, pumpTypeId,
|
|
|
+ UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
|
|
|
+ UA_QUALIFIEDNAME(1, "MotorRPMs"),
|
|
|
+ UA_NODEID_NULL, rpmAttr, NULL, NULL);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Now we add the derived ObjectType for the pump that inherits from the device
|
|
|
+ * object type. The resulting object contains all four inherited child
|
|
|
+ * variables. The object has a reference of type ``hasTypeDefinition`` to the
|
|
|
+ * object type. Clients can browse this information at runtime and adjust
|
|
|
+ * accordingly.
|
|
|
+ */
|
|
|
+
|
|
|
+static void
|
|
|
+addPumpObjectInstance(UA_Server *server, char *name) {
|
|
|
+ UA_ObjectAttributes oAttr;
|
|
|
+ UA_ObjectAttributes_init(&oAttr);
|
|
|
+ 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.
|
|
|
+ */
|
|
|
+
|
|
|
+UA_Server *s = NULL; /* required to get the server pointer into the constructor
|
|
|
+ function (will change for v0.3) */
|
|
|
+
|
|
|
+static void *
|
|
|
+pumpTypeConstructor(const UA_NodeId instance) {
|
|
|
+ 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 = instance;
|
|
|
+ bp.relativePath.elementsSize = 1;
|
|
|
+ bp.relativePath.elements = &rpe;
|
|
|
+
|
|
|
+ UA_BrowsePathResult bpr =
|
|
|
+ UA_Server_translateBrowsePathToNodeIds(s, &bp);
|
|
|
+ if(bpr.statusCode != UA_STATUSCODE_GOOD ||
|
|
|
+ bpr.targetsSize < 1)
|
|
|
+ return NULL;
|
|
|
+
|
|
|
+ /* Set the status value */
|
|
|
+ UA_Boolean status = true;
|
|
|
+ UA_Variant value;
|
|
|
+ UA_Variant_setScalar(&value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
|
+ UA_Server_writeValue(s, bpr.targets[0].targetId.nodeId, value);
|
|
|
+ UA_BrowsePathResult_deleteMembers(&bpr);
|
|
|
+
|
|
|
+ /* The return pointer of the constructor is attached to the ObjectNode */
|
|
|
+ return NULL;
|
|
|
+}
|
|
|
+
|
|
|
+static void
|
|
|
+addPumpTypeConstructor(UA_Server *server) {
|
|
|
+ UA_ObjectLifecycleManagement olm;
|
|
|
+ olm.constructor = pumpTypeConstructor;
|
|
|
+ olm.destructor = NULL;
|
|
|
+ UA_Server_setObjectTypeNode_lifecycleManagement(server, pumpTypeId, olm);
|
|
|
+}
|
|
|
+
|
|
|
+/** 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_standard;
|
|
|
+ UA_ServerNetworkLayer nl =
|
|
|
+ UA_ServerNetworkLayerTCP(UA_ConnectionConfig_standard, 16664);
|
|
|
+ config.networkLayers = &nl;
|
|
|
+ config.networkLayersSize = 1;
|
|
|
+ UA_Server *server = UA_Server_new(config);
|
|
|
+ s = server; /* required for the constructor */
|
|
|
+
|
|
|
+ manuallyDefinePump(server);
|
|
|
+ defineObjectTypes(server);
|
|
|
+ addPumpObjectInstance(server, "pump2");
|
|
|
+ addPumpObjectInstance(server, "pump3");
|
|
|
+ addPumpTypeConstructor(server);
|
|
|
+ addPumpObjectInstance(server, "pump4");
|
|
|
+ addPumpObjectInstance(server, "pump5");
|
|
|
+
|
|
|
+ UA_Server_run(server, &running);
|
|
|
+ UA_Server_delete(server);
|
|
|
+ nl.deleteMembers(&nl);
|
|
|
+ return 0;
|
|
|
+}
|