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.
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
};
struct variable_info {
int id;
int parent;
const char *name;
enum vartype type;
const char *rtaddress;
const char *owner;
const char *group;
uint32_t permissions;
};
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:
for (i = 0; i < NUM_STRINGS; ++i) {
}
ua_file_namespace_init(&ns);
ns.nsidx = 0;
ns.nsuri = "http://opcfoundation.org/UA/";
ua_file_namespace_clear(&ns);
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++;
switch (g_addrspace[i].type) {
case VARTYPE_FOLDER:
ns.stat.num_objects++;
break;
case VARTYPE_VARIABLE:
case VARTYPE_PROPERTY:
ns.stat.num_variables++;
break;
}
ns.stat.num_references += 2;
}
w.stat = ns.stat;
ua_file_namespace_clear(&ns);
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.
{
unsigned int i;
struct variable_info *var;
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);
}
for (i = 0; i < countof(g_addrspace); ++i) {
var = &g_addrspace[i];
if (var->type == VARTYPE_FOLDER)
create_folder(w, var);
}
}
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.
void create_folder(
struct ua_file_writer *w,
struct variable_info *var)
{
var->owner,
var->group,
var->permissions
};
dn.text = string_index(var->name);
bn.nsidx = FRUITS_NSIDX;
bn.name = dn.text;
ua_file_object_init(&folder);
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);
ua_file_basenode_add_extension(&folder.common, &ex);
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.
void create_variable(
struct ua_file_writer *w,
struct variable_info *var)
{
var->owner,
var->group,
var->permissions
};
dn.text = string_index(var->name);
bn.nsidx = FRUITS_NSIDX;
bn.name = dn.text;
ua_file_variable_init(&variable);
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);
ua_file_variable_set_datatype(&variable, &dtid);
switch (var->dt) {
case UA_VT_INT32:
break;
case UA_VT_FLOAT:
break;
case UA_VT_DOUBLE:
break;
default:
break;
}
ua_file_variable_set_value(&variable, &default_value);
ua_file_basenode_add_extension(&variable.common, &ex1);
ua_file_basenode_add_extension(&variable.common, &ex2);
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.
- we create the organizing references which reflect the hierarchy of our data model
- we add the HasTypeDefinition references which are required by UA and define the semantic type of our nodes.
{
unsigned int i;
struct variable_info *var, *parent;
struct ua_nodeid src, dst, hastypedef, organizes, foldertype, variabletype, propertytype;
for (i = 0; i < countof(g_addrspace); ++i) {
var = &g_addrspace[i];
if (var->parent >= 0) {
parent = &g_addrspace[var->parent];
} else {
}
switch (var->type) {
case VARTYPE_FOLDER:
break;
case VARTYPE_VARIABLE:
break;
case VARTYPE_PROPERTY:
break;
}
}
}