High Performance OPC UA Server SDK  1.7.1.383
Tagfile Server Example

The purpose of this example is to demonstrate the tagfile functionality of the binary file format using the RTAddress extension.

The UA binary file format allows to add custom extensions to each node with additional information. The extension used in this example is the RTAddress, which allows to store additional implementation specific address information as string.

In this example we assume that the server uses a Modbus connection as underlying data source. Instead of a real Modbus communication, which would require a Modbus device, the implemented modbus_store uses simulation data, but shows how to access data using the Modbus address from tagfile.bin.

The provider_tagfile loads the binary file tagfile.bin which is created from tagfile.xml using xml2bin.

Files used in this lesson:

Sample Data

The file tagfile.xml contains the following variables:

DisplayName NodeId DataType Modbus Address
Temperature ns=1;i=6001 UInt32 40004
OilPressure ns=1;i=6002 UInt32 40002
Current ns=1;i=6003 UInt32 40001
Speed ns=1;i=6004 UInt32 40003

XML Extension

The RTAddress information can be added as XML extension to the nodeset XML file as shown in the example below. The tool xml2bin understands this extension and converts it into its binary equivalent of the UA Binary File Format.

<UAVariable DataType="UInt32" NodeId="ns=1;i=6001" BrowseName="1:Temperature" AccessLevel="3">
<DisplayName>Temperature</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=63</Reference>
<Reference ReferenceType="HasComponent" IsForward="false">ns=1;i=5001</Reference>
</References>
<Extensions>
<Extension>
<ua:RTAddress>modbus://40004</ua:RTAddress>
</Extension>
</Extensions>
</UAVariable>

Generating the Binary Tagfile

The example contains the usual gen.sh script to generate the bin file from the XML source. You can also manually generate the file using this command:

./xml2bin -n2 -o tagfile.bin Opc.Ua.NodeSet2.xml tagfile.xml

How to generate binary files with extensions programmatically using he UA File Format Library is shown in the example File Writer Example.

Inspecting the Generated File

You can use the tool filetest to inspect the content of binary files like this:

./filetest -v tagfile.bin

Loading binary files with extensions

The function ua_addressspace_load_file_ext() can be used to load binary files and process the extensions. Unlike the simpler function ua_addressspace_load_file(), this function allows specifying callbacks, which are defined in ua_file_callbacks.

Example:

ua_file_callbacks_init(&cb);
cb.add_extension_namespace = provider_tagfile_add_extension_namespace;
cb.add_extension = provider_tagfile_add_extension;
TRACE_INFO(TRACE_FAC_PROVIDER, "Loading namespace from binary file '%s'\n", filename);
ret = ua_addressspace_load_file_ext(filename, &g_staticstore, &config, &cb);
if (ret < 0) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Loading namespace from file failed: %i (%s)\n", ret, util_error_lookup(ret));
return ret;
}
nsidx = (uint16_t)ret;

The callbacks add_extension_namespace and add_extension are used in this example for processing the RTAddress extension. Extensions are organized in namespaces (like UA namespaces) to avoid ambiguities in their numeric extension type. The extensions defined by Unified Automation are defined in the namespace UA_EXTENSION_UNIFIEDAUTOMATION_URI.

Mapping the Namespace URI

The first step is mapping the expected extension URI to it's numeric namespace. Every time the add_extension_namespace is called we check for the URI and store the numeric nsidx for this URI when found.

static void provider_tagfile_add_extension_namespace(uint16_t nsidx, const char *uri)
{
/* look for namespace URI we are interested in */
if (strcmp(uri, UA_EXTENSION_UNIFIEDAUTOMATION_URI) == 0) {
g_unified_nsidx = nsidx;
}
}

Processing the Extensions

The next step is processing the extensions itself. Therefor we check for the extension's nsidx and it's type (UA_EXTENSION_RUNTIMEADDRESS). When found we process this runtime address.

In this example we know it is a Modbus address with this syntax: "modbus://<address>". We use sscanf() to parse this address and register the variable at the modbus_store as show in this code snippet.

Modbus Address Parsing:

/* handle runtime address as an example */
ua_file_extension_get_runtimeaddress(reg, ext, &rtaddress);
TRACE_NOTICE(TRACE_FAC_PROVIDER, "The runtime address of node %08x is '%" UA_STRING_FMT "'.\n", node,
UA_STRING_ARGS(&rtaddress));
/* parse runtime address */
ret = sscanf(ua_string_const_data(&rtaddress), "modbus://%u", &modbus_address);
if (ret == 1) {
/* register variable at modbus store */
ua_modbusstore_register_node(&g_modbusstore, modbus_address, 0, node);
}

The complete callback implementation including the extension filtering code and additional trace information looks like this:

static void provider_tagfile_add_extension(ua_node_t node, struct ua_file_extension_registry *reg, struct ua_file_extension *ext)
{
#ifdef HAVE_TRACE
const char *unified_extension_names[] = { "USERDB", "GROUPDB", "PASSDB", "GENERATOR", "ENGINFO", "AUTHORIZATION",
"RUNTIMEADDRESS", "DOCUMENTATION", "XML" };
#endif
#ifndef HAVE_TRACE
UA_UNUSED(node);
#endif
/* check if it's one of our extensions */
if (ext->nsidx == g_unified_nsidx) {
#ifdef HAVE_TRACE
const char *ext_name = "unknown";
#endif
struct ua_string rtaddress;
struct ua_string xml;
unsigned int modbus_address;
int ret;
/* resolve type into string for tracing */
#ifdef HAVE_TRACE
if (ext->type < countof(unified_extension_names)) ext_name = unified_extension_names[ext->type];
/* we only write a trace output */
TRACE_NOTICE(TRACE_FAC_PROVIDER, "Node %08x contains the Unified Automation extension %s(%u).\n", node, ext_name, ext->type);
#endif
/* handle runtime address as an example */
ua_file_extension_get_runtimeaddress(reg, ext, &rtaddress);
TRACE_NOTICE(TRACE_FAC_PROVIDER, "The runtime address of node %08x is '%" UA_STRING_FMT "'.\n", node,
UA_STRING_ARGS(&rtaddress));
/* parse runtime address */
ret = sscanf(ua_string_const_data(&rtaddress), "modbus://%u", &modbus_address);
if (ret == 1) {
/* register variable at modbus store */
ua_modbusstore_register_node(&g_modbusstore, modbus_address, 0, node);
}
} else if (ext->type == UA_EXTENSION_XML) {
/* handle generic XML extension */
ua_file_extension_get_xml(reg, ext, &xml);
TRACE_NOTICE(TRACE_FAC_PROVIDER, "XML extension: '%" UA_STRING_FMT "'.\n", UA_STRING_ARGS(&xml));
}
} else {
const char *uri = ua_file_extension_registry_get_uri(reg, ext->nsidx);
if (uri) {
TRACE_NOTICE(TRACE_FAC_PROVIDER, "Node %08x contains a unknown extension ns=%s, type=%u.\n", node, uri, ext->type);
} else {
"Node %08x contains an extension with nsdix=%u, which is not part of the namespace table. This is in invalid file!\n",
node, ext->nsidx);
}
}
}