High Performance OPC UA Server SDK  1.2.0.193
Sensor Model Server

This example shows how to create an address space from a binary address space file containing some types and how to create instances from these types at runtime.

Therefor we created a simple information model using UaModeler and exported this model as XML file. This XML file is then converted into a binary file using the provided xml2bin tool.

The Model

The model contains an instance of FolderType called Sensors below Objects which will be the root entry point of all our sensor instances. Below BaseObjectType we created a type hierarchy for our sensors consisting of an abtract BaseSensorType and a TemperatureSensorType. The TemperatureSensorType contains a process value named CurrentTemperature which is an instance of AnalogItemType. The meta information EngineeringUnits and EURange is defined already in the type and all instances will automatically get the same values. For this reason the AccessLevel is defined ReadOnly. This is a hint for the ua_object_create_instance function which will reuse the same value for all instances if it is ReadOnly. The value of CurrentTemperature will be connected to a custom value store in our code.

The following image shows how this model looks like in UaModeler:

sensor_model.png
Information Model in UaModeler

When the example is compiled and running we can connect with UaExpert. As you can see in the following screenshot the address space contains 10 instances of the TemperatureSensorType. On the right side in the attributes window you can see the unit of the select EngineeringUnits property of one sensor instance. In the data access view in the center of the screen you can see subscribed temperature values with values provided by the custom value store.

sensor_uaexpert.png
Resulting Server Address Space shown in UaExpert

Files

The following table describes the files of this example.

File Description
sensormodel.tt2pro UaModeler project.
sensormodel.ua UaModeler binary information model.
sensormodel.xml The sensormodel.ua exported as XML.
sensormodel.bin Binary address space file generated with xml2bin.
gen.sh Helper script to generate sensormodel.bin from sensormodel.xml
server_main.c Main application
provider_sensor.* The sensor provider implementation which loads the sensormodel.bin file.
provider_sensor_store.* The custom store for sensor values.
sensordemo_indentifiers.h Contains some defines for numeric nodeids for the demo model.

Loading the Address Space

Loading the binary address space file is simply done by calling ua_addressspace_load_file. Normally the allocated memory pools are exactly as big as required for the model that gets loaded. Because we know that we want to add additional nodes to this namespace dynamically we pass this additional memory as the last parameter to ua_addressspace_load_file. Therefor we create the variable config which contains the additional memory requirements.

The following code fragment is part of provider_sensor_init() in provider_sensor.c:

Configuring Memory of additional dynamic nodes (for create_instances):

/* reserve additional memory for dynamically created nodes */
ua_memset(&config, 0, sizeof(config));
config.max_variables = 50; /* additional variables */
config.max_objects = 50; /* additional objects */
config.max_references = 150; /* additional references */
config.max_strings = 150; /* additional strings */
config.nsidx = UA_NSIDX_INVALID; /* auto assign */

Loading the binary file:

ret = ua_staticstore_init(&g_staticstore, 0);
if (ret < 0) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Failed to initialized static store: %i (%s)\n", ret, util_error_lookup(ret));
return ret;
}
TRACE_INFO(TRACE_FAC_PROVIDER, "Loading NS from binary file 'sensormodel.bin'\n");
ret = ua_addressspace_load_file("sensormodel.bin", &g_staticstore, &config);
if (ret < 0) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Loading NS from file 'sensormodel.bin' failed: %i (%s)\n", ret, util_error_lookup(ret));
return ret;
}
/* assign nsidx of loaded dynamic address space */
uaprovider_sensor_nsidx = (uint16_t) ret;
/* we will create the instances in the same namespace */
uaprovider_sensorinstance_nsidx = (uint16_t) ret;

Alternative: Statically linked Address Space

We can also statically link the address space instead of loading the binary file into RAM. But then we cannot create instances anymore in this address space. Instead we create another dynamic address space for the instances.

Loading the statically linked address space.

TRACE_INFO(TRACE_FAC_PROVIDER, "Loading static NS (sensormodel) from built-in data\n");
/* call gernerated function from ns2.c */
ret = sensor_register_static_addressspace();
if (ret < 0) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Loading static addressspace failed: %i (%s)\n", ret, util_error_lookup(ret));
return ret;
}
/* assign nsidx of loaded static address space */
uaprovider_sensor_nsidx = (uint16_t) ret;
/* call gernerated function from ns2.c */
ret = sensor_initialize_staticstore(&g_staticstore, SENSOR_MODEL_STATIC_STORE_INDEX);
if (ret < 0) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Loading staticstore failed: %i (%s)\n", ret, util_error_lookup(ret));
return ret;
}

Creating a 3rd dynamic address space for the instances.

/* register 3rd namespace for dynamic instances */
config.nsidx = SENSOR_MODEL_NAMESPACE_INDEX + 1;
ret = ua_addressspace_register(SENSOR_MODEL_NAMESPACE_URI "Instances", &config, UA_INDEX_HASHTABLE);
uaprovider_sensorinstance_nsidx = (uint16_t) ret;
/* register this provider IF for the instance address space */
ret = uaprovider_register_nsindex(ctx, uaprovider_sensorinstance_nsidx);
if (ret < 0) return ret;

Note that we concatenated the generated string constants SENSOR_MODEL_NAMESPACE_URI with "Instances", which results in the URI http://www.unifiedautomation.com/SensorModel/Instances.

Creating Instances of TemperatureSensorType

Creating the instances is done in function provider_sensor_create_instance which is also called from provider_sensor_init after the address space was successfully loaded.

Therefor we retrieve the node handles of the required nodes by there well-known numeric nodeid. Then we create a string nodeid dynamically for our new instance and call ua_object_create_instance. The whole process of creating a node is done in a for loop. The number of instances can be configured using the define NUM_SENSOR_INSTANCES.

int provider_sensor_create_instances(uint16_t type_nsidx, uint16_t instance_nsidx)
{
int ret = 0, i;
struct ua_nodeid id;
ua_node_t obj;
ua_node_t sensors;
struct ua_string strnodeid;
const char *browsename = "TemperatureSensorType";
const char *displayname;
/* find sensors folder */
sensors = ua_node_find_numeric(type_nsidx, SENSOR_ID_SENSORS);
if (sensors == UA_NODE_INVALID) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Could not find Sensors folder node.");
return -1;
}
/* find type */
type = ua_node_find_numeric(type_nsidx, SENSOR_ID_TEMPERATURESENSORTYPE);
if (type == UA_NODE_INVALID) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Could not find TemperatureSensorType node.");
return -1;
}
/* create instances */
ua_string_init(&strnodeid);
for (i = 0; i < NUM_SENSOR_INSTANCES; ++i) {
/* create string nodeid */
ua_string_snprintf(&strnodeid, 30, "TemperatureSensor%03u", i);
ua_nodeid_set_string(&id, instance_nsidx, ua_string_const_data(&strnodeid));
/* displayname is the same */
displayname = ua_string_const_data(&strnodeid);
/* create an instance of TemperatureSensorType */
obj = ua_object_create_instance(&id, type, browsename, displayname);
if (obj == UA_NODE_INVALID) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Failed to create instance of TemperatureSensorType");
ua_string_clear(&strnodeid);
return -1;
}
/* add organizes reference form sensors folder to new instance */
ret = ua_reference_add(sensors, obj, UA_NODE_ORGANIZES);
if (ret == UA_REF_INVALID) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Failed add reference from Sensors to TemperatureSensor");
ua_string_clear(&strnodeid);
return -1;
}
}
ua_string_clear(&strnodeid);
return 0;
}

Connecting Values with a Custom Store

Connecting the variables with a value store is simply done by setting the store id and the value index by using ua_variable_set_store_index and ua_variable_set_value_index.

The complicated part in this example is that we don't know the nodeids if our CurrentTemperature variables. These nodes will be created dynamically by ua_object_create_instance. But we know the nodeids of the sensor instances, that we created ourself in provider_sensor_create_instance, and we know the BrowseName of the CurrentTemperature variables. So we iterate over all references of the sensor instance and compare the browsename of the referenced nodes. When the CurrentTemperature is found we set the store information and call break to exit the foreach loop.

int provider_sensor_store_init(uint16_t type_nsidx, uint16_t instance_nsidx) {
struct ua_valuestore_interface store_if;
uint8_t store_idx;
ua_node_t node, var;
ua_ref_t ref;
struct ua_nodeid id;
struct ua_string strnodeid;
int ret;
int i;
/* register custom store */
store_if.get_fct = provider_sensor_store_get_value;
store_if.attach_fct = provider_sensor_store_attach_value;
store_idx = 0; /* let the SDK assign an index */
ret = ua_valuestore_register_store(&store_if, &store_idx);
if (ret != 0) return ret;
g_type_nsidx = type_nsidx;
g_instance_nsidx = instance_nsidx;
/* create browsename for CurrentTemperature variables */
ua_qualifiedname_set(&currentvalue, type_nsidx, "CurrentTemperature");
ua_qualifiedname_set(&position, type_nsidx, "Position");
ua_qualifiedname_set(&serialnumber, type_nsidx, "SerialNumber");
ua_string_init(&strnodeid);
/* assign store indices */
for (i = 0; i < NUM_SENSOR_INSTANCES; ++i) {
/* create string nodeid */
ua_string_snprintf(&strnodeid, 30, "TemperatureSensor%03u", i);
ua_nodeid_set_string(&id, instance_nsidx, ua_string_const_data(&strnodeid));
node = ua_node_find(&id);
if (node == UA_NODE_INVALID) break;
/* find CurrentTemperature variable below sensor */
ua_node_foreach(ref, node) {
var = ua_reference_target(ref);
ret = ua_node_get_browsename(var, &browsename);
if (ret != 0) continue;
if (ua_qualifiedname_compare(&browsename, &currentvalue) == 0) {
/* configure store index for variable */
ret = ua_variable_set_store_index(var, store_idx);
if (ret != 0) goto error;
if (ret != 0) goto error;
} else if (ua_qualifiedname_compare(&browsename, &position) == 0) {
/* configure store index for variable */
ret = ua_variable_set_store_index(var, store_idx);
if (ret != 0) goto error;
ret = ua_variable_set_value_index(var, i+1000);
if (ret != 0) goto error;
} else if (ua_qualifiedname_compare(&browsename, &serialnumber) == 0) {
/* configure store index for variable */
ret = ua_variable_set_store_index(var, store_idx);
if (ret != 0) goto error;
ret = ua_variable_set_value_index(var, i+2000);
if (ret != 0) goto error;
}
ua_qualifiedname_clear(&browsename);
}
/* fill demo values */
g_sensor_temperatures[i] = 1.23 * i;
g_sensor_positions[i].x = 1 * i;
g_sensor_positions[i].y = 1.1 * i;
g_sensor_positions[i].z = 1.2 * i;
util_snprintf(g_sensor_serials[i], SERIAL_LENGTH, "5000-%04u", i);
}
ua_string_clear(&strnodeid);
ua_qualifiedname_clear(&currentvalue);
return 0;
error:
ua_string_clear(&strnodeid);
ua_qualifiedname_clear(&currentvalue);
ua_qualifiedname_clear(&browsename);
return -1;
}

The store getter function simply returns a double value that was created as a global double array.

void provider_sensor_store_get_value(void *store, ua_node_t node, unsigned int idx, bool source_ts, struct ua_indexrange *range, unsigned int num_ranges, struct ua_datavalue *result)
{
unsigned int variable_type = idx / 1000; /* 0=temp, 1=position, 2=serial */
struct ua_nodeid vector_typeid = UA_NODEID_NUMERIC_INITIALIZER(SENSOR_ID_VECTOR, g_type_nsidx);
idx = idx % 1000;
UA_UNUSED(node);
UA_UNUSED(range);
UA_UNUSED(store);
/* check array bounds */
if (idx >= NUM_SENSOR_INSTANCES) {
return;
}
if (variable_type >= 3) {
return;
}
/* there are only scalar values in the store */
if (num_ranges > 0) {
return;
}
/* write value to result */
switch (variable_type) {
case 0:
ua_variant_set_double(&result->value, g_sensor_temperatures[idx]);
break;
case 1:
ua_variant_set_extensionobject(&result->value, &g_sensor_positions[idx], &vector_typeid);
break;
case 2:
ua_variant_set_string(&result->value, g_sensor_serials[idx]);
break;
}
/* add source timestamp if requested */
if (source_ts) ua_datetime_now(&result->source_timestamp);
/* set good statuscode */
result->status = 0;
}