High Performance OPC UA Server SDK  1.2.1.203
File Writer Example

In this example we demonstrate how to create a UA Binary File from some existing information using the UA File Writer API.

In addition we show how to add custom node extension to enrich the UA information model with application specific data.

This approach can be used e.g. by engineering tools which create UA Binary Files to configure OPC UA Servers.

Topics covered in this example:

  • Using the File Writer API
  • Mapping custom address space information to UA models
  • Adding runtime addresses to nodes
  • Adding authorization information to nodes (access permissions)

The Existing Data Model

The example data model which is the source for creating an UA address space consists of a simple hierarchy, which can contain folders, variables and properties.

filewriter_model.png
Example Data Model

In addition variables can contain a runtime address, that will be stored as a node extension in the binary file, and access permissions which will also be added as node extension.

enum vartype {
VARTYPE_FOLDER,
VARTYPE_VARIABLE,
VARTYPE_PROPERTY
};
/* Simple struct to hold info about variables.
* We use this to create the UA variables.
*/
struct variable_info {
int id; /* numeric nodeid */
int parent; /* parent index, -1 if no parent */
const char *name; /* variable name */
enum vartype type; /* variable type */
enum ua_variant_type dt; /* variable datatype */
const char *rtaddress; /* runtime address of underlying system */
/* authorization info */
const char *owner;
const char *group;
uint32_t permissions;
};
/* demo address space */
struct variable_info g_addrspace[] = {
{ FRUITS_ID_FRUITS, -1, "Fruits", VARTYPE_FOLDER, UA_VT_NULL, NULL, "root", "operators", UA_FILE_EXT_AUTH_PERM_OWNER_ALL | UA_FILE_EXT_AUTH_PERM_GROUP_OBSERVATION | UA_FILE_EXT_AUTH_PERM_OTHERS_OBSERVATION },
{ FRUITS_ID_APPLE, 0, "Apple", VARTYPE_VARIABLE, UA_VT_DOUBLE, "I17", "joe", "users", UA_FILE_EXT_AUTH_PERM_OWNER_ALL | UA_FILE_EXT_AUTH_PERM_GROUP_OBSERVATION | UA_FILE_EXT_AUTH_PERM_OTHERS_BROWSEABLE },
{ FRUITS_ID_BANANNA, 0, "Bananna", VARTYPE_VARIABLE, UA_VT_FLOAT, "Q23", "root", "operators", UA_FILE_EXT_AUTH_PERM_OWNER_ALL | UA_FILE_EXT_AUTH_PERM_GROUP_OPERATION | UA_FILE_EXT_AUTH_PERM_OTHERS_OBSERVATION },
{ FRUITS_ID_LEMON, 0, "Lemon", VARTYPE_PROPERTY, UA_VT_INT32, "M58", "sue", "users", UA_FILE_EXT_AUTH_PERM_OWNER_ALL | UA_FILE_EXT_AUTH_PERM_GROUP_OPERATION | UA_FILE_EXT_AUTH_PERM_OTHERS_OBSERVATION },
};

Creating a new UA Binary File

Using the File Writer API is quiet easy, your only need to follow the defined order of API calls to create the correct file layout as defined in the specification (see OPC UA Binary File Format).

It starts by creating a new file writer context, and the fill the meta data for the file, before opening the file for writing. The meta data contains information about the number of nodes for each nodeclass, namespaces and string tables.

After opening the file for writing your can add all the nodes. Here it is important to use the correct order (see Adding the Nodes). At the end you add all the references which connect the nodes and close file.

The following example demonstrates the usage:

/* initialize file writer context */
/* START META DATA: fill meta data before opening the file for writing. */
/* create string tables */
ua_file_writer_add_stringtables(&w, NUM_STRINGS, "en-US;it-IT");
for (i = 0; i < NUM_STRINGS; ++i) {
ua_file_writer_add_string(&w, 0, i, (char*)g_strings_en[i]);
ua_file_writer_add_string(&w, 1, i, (char*)g_strings_it[i]);
}
/* add required namespaces: This demo depends on NS0 only */
ua_file_namespace_init(&ns);
ns.nsidx = 0;
ns.nsuri = "http://opcfoundation.org/UA/";
ua_file_namespace_clear(&ns);
/* add provided namespaces: This demo creates one new namespace */
ua_file_namespace_init(&ns);
ns.nsidx = FRUITS_NSIDX;
ns.nsuri = FRUITS_NSURI;
for (i = 0; i < countof(g_addrspace); ++i) {
ns.stat.num_numerics++; /* count numeric nodeids */
switch (g_addrspace[i].type) {
case VARTYPE_FOLDER:
ns.stat.num_objects++; /* count objects */
break;
case VARTYPE_VARIABLE:
case VARTYPE_PROPERTY:
ns.stat.num_variables++; /* count variables */
break;
}
ns.stat.num_references += 2; /* count numeric nodeids */
}
/* set total number of nodes for global file header: this is the sum of
* the provided ns stat entries. We only add one namespace so this is identical
* as above.
*/
w.stat = ns.stat;
ua_file_namespace_clear(&ns);
/* create extension registry: this is necessary for adding extensions */
/* END META DATA */
/* open/create the file for writing, this will implicitly write the
* file header, global extensions, string tables, and namesapce tables. */
ua_file_writer_open(&w, filename);
create_nodes(&w);
create_references(&w);

Adding the Nodes

Now we create the nodes based on the variable_info struct of our data model. Because we must add the nodes in correct order (by nodeclass) to the file we need to filter our data and add the nodes by using some helper functions. These helper function are doing the mapping from the existing data model to UA.

void create_nodes(struct ua_file_writer *w)
{
unsigned int i;
struct variable_info *var;
/* 1. Add DataType nodes */
/* not used */
/* 2. Add ReferenceType nodes */
/* not used */
/* 3. Add VariableType nodes */
/* not used */
/* 4. Add ObjectType nodes */
/* not used */
/* 5. Add Variable nodes */
for (i = 0; i < countof(g_addrspace); ++i) {
var = &g_addrspace[i];
if (var->type == VARTYPE_VARIABLE || var->type == VARTYPE_PROPERTY)
create_variable(w, var);
}
/* 6. Add Object nodes */
for (i = 0; i < countof(g_addrspace); ++i) {
var = &g_addrspace[i];
if (var->type == VARTYPE_FOLDER)
create_folder(w, var);
}
/* 7. Add Method nodes */
/* not used */
/* 8. Add View nodes */
/* not used */
}

For creating folders we use the helper function create_folder. We use the variable name to create UA browsename, displayname and description. The id field is used to create a numeric NodeId.

In addition to the base attributes this function adds also the authorization extension, based on the information provided by our data model.

/* Creates a UA folder based on variable table entries. */
void create_folder(struct ua_file_writer *w, struct variable_info *var)
{
struct ua_nodeid id;
struct ua_file_object folder;
struct ua_file_extension ex;
var->owner,
var->group,
var->permissions
};
/* create nodeid */
ua_nodeid_set_numeric(&id, FRUITS_NSIDX, var->id);
/* use variable name as UA displayname */
dn.text = string_index(var->name);
/* use variable name as UA browsename */
bn.nsidx = FRUITS_NSIDX; /* index of namespace where this QN is defined in */
bn.name = dn.text; /* same string as for lt */
/* prepare authorization extension */
ua_file_object_init(&folder);
/* fill base attributes */
ua_file_basenode_set_nodeid(&folder.common, &id);
ua_file_basenode_set_browsename(&folder.common, &bn);
ua_file_basenode_set_displayname(&folder.common, &dn);
ua_file_basenode_set_description(&folder.common, &dn);
/* add example extension with access permissions */
ua_file_basenode_add_extension(&folder.common, &ex);
/* add object to file */
/* cleanup */
ua_file_object_clear(&folder);
ua_file_extension_clear(&ex);
}

For creating variables and properties we use the helper function create_variable. The base attributes are set in the same way as for the folder, but in addition we also add the variable datatype, valuerank and the runtime address extension.

/* Creates a UA variable based on variable table entries. */
void create_variable(struct ua_file_writer *w, struct variable_info *var)
{
struct ua_nodeid id, dtid;
struct ua_file_variable variable;
struct ua_file_extension ex1, ex2;
var->owner,
var->group,
var->permissions
};
struct ua_string rtaddress;
struct ua_variant default_value;
ua_string_attach_const(&rtaddress, var->rtaddress);
/* create nodeid */
ua_nodeid_set_numeric(&id, FRUITS_NSIDX, var->id);
/* use variable name as UA displayname */
dn.text = string_index(var->name);
/* use variable name as UA browsename */
bn.nsidx = FRUITS_NSIDX; /* index of namespace where this QN is defined in */
bn.name = dn.text; /* same string as for lt */
/* create datatype nodeid */
ua_nodeid_set_numeric(&dtid, 0, var->dt);
/* prepare authorization extension */
ua_file_extension_set_authorization(&w->reg, &ex1, &auth);
/* prepare runtime address extension */
ua_file_extension_set_runtimeaddress(&w->reg, &ex2, &rtaddress);
ua_file_variable_init(&variable);
/* fill base attributes */
ua_file_basenode_set_nodeid(&variable.common, &id);
ua_file_basenode_set_browsename(&variable.common, &bn);
ua_file_basenode_set_displayname(&variable.common, &dn);
ua_file_basenode_set_description(&variable.common, &dn);
/* fill variable attributes */
ua_file_variable_set_datatype(&variable, &dtid);
ua_file_variable_set_valuerank(&variable, UA_VALUERANK_SCALAR);
switch (var->dt) {
case UA_VT_INT32:
ua_variant_set_int32(&default_value, 17);
break;
case UA_VT_FLOAT:
ua_variant_set_float(&default_value, 2.345f);
break;
case UA_VT_DOUBLE:
ua_variant_set_double(&default_value, 1.23);
break;
default:
ua_variant_init(&default_value);
break;
}
ua_file_variable_set_value(&variable, &default_value);
ua_file_variable_set_accesslevel(&variable, UA_ACCESSLEVEL_CURRENTREADWRITE);
/* add extensions */
ua_file_basenode_add_extension(&variable.common, &ex1);
ua_file_basenode_add_extension(&variable.common, &ex2);
/* add variable to file */
/* cleanup */
ua_file_variable_clear(&variable);
ua_file_extension_clear(&ex1);
ua_file_extension_clear(&ex2);
}

Adding the References

The last step is to add the references. This consist of two parts.

  1. we create the organizing references which reflect the hierarchy of our data model
  2. we add the HasTypeDefinition references which are required by UA and define the semantic type of our nodes.
void create_references(struct ua_file_writer *w)
{
unsigned int i;
struct variable_info *var, *parent;
struct ua_nodeid src, dst, hastypedef, organizes, foldertype, variabletype, propertytype;
ua_nodeid_set_numeric(&hastypedef, 0, UA_ID_HASTYPEDEFINITION);
ua_nodeid_set_numeric(&organizes, 0, UA_ID_ORGANIZES);
ua_nodeid_set_numeric(&foldertype, 0, UA_ID_FOLDERTYPE);
ua_nodeid_set_numeric(&variabletype, 0, UA_ID_BASEVARIABLETYPE);
ua_nodeid_set_numeric(&propertytype, 0, UA_ID_PROPERTYTYPE);
for (i = 0; i < countof(g_addrspace); ++i) {
var = &g_addrspace[i];
/* create parent-child relation */
if (var->parent >= 0) {
parent = &g_addrspace[var->parent];
ua_nodeid_set_numeric(&src, FRUITS_NSIDX, parent->id);
} else {
/* put nodes without parent into Objects folder */
ua_nodeid_set_numeric(&src, 0, UA_ID_OBJECTSFOLDER);
}
ua_nodeid_set_numeric(&dst, FRUITS_NSIDX, var->id);
ua_file_writer_add_reference(w, &src, &dst, &organizes);
/* create typedefinition reference */
ua_nodeid_set_numeric(&src, FRUITS_NSIDX, var->id);
switch (var->type) {
case VARTYPE_FOLDER:
ua_file_writer_add_reference(w, &src, &foldertype, &hastypedef);
break;
case VARTYPE_VARIABLE:
ua_file_writer_add_reference(w, &src, &variabletype, &hastypedef);
break;
case VARTYPE_PROPERTY:
ua_file_writer_add_reference(w, &src, &propertytype, &hastypedef);
break;
}
}
}