High Performance OPC UA Server SDK  1.5.1.314
File Transfer

The SDK implements the UA FileType as defined by "Annex C File Transfer" - "OPC UA Specification - Part 5: Information Model".

Background

The purpose of the FileType is to be able to transfer file contents, that might be exceeding the maximum message size of UA by adding methods which allow to read and write parts of files. In addition it provides functionality for locking files and read file meta data. See specification for more details on the FileType itself.

To understand the SDK architecture of the FileType you must first understand that the FileType that is visible via OPC UA address space is independent from the technical filetype used in the code. So it is possible that you have multiple instances of the same UA FileType, which use different file implementations. So one instance might be connected with a real filesystem file, another instance might be an in-memory file.

To achieve this the SDK defines a ua_filetype_interface, which is a calltable to the actual file implementation, and a ua_file_context, which represents one instance of a real file. This ua_file_context holds file meta data as well as the ua_filetype_interface to use for this instance. The UA node in the address space, which represents a file, is connected to such a ua_file_context. The type ua_file_handle represents and opened file with its meta data like e.g. the file position.

filetype.png
FileSystem Model

The SDK infrastructure takes care of handling UA file method invocations, locking and cleanup of file resources. E.g. if a session gets deleted or a file is not used for a configurable time (file_timeout), then the file gets closed automatically and the associated file handle gets removed.

Creating File Instances

The SDK provides two default implementations of a FileType.

  1. ua_filesystem_implementation : This uses real files and implements access to those files using the platform layer (PL) file API. This means, if your PL provides a working file API, then also ua_filesystem_implementation is working. Because this API depends on the option SUPPORT_FILE_IO, also the ua_filesystem_implementation SUPPORT_FILE_IO depends on this option and is unavailable if this option was disabled.
  2. ua_memoryfile_implementation : Represents an in-memory file based on a ua_bytestring.

Creating a FileSystem File Instance

The function ua_filetype_create_ex does two things in one step. It creates the instance of the specified file type (UA_NODE_FILETYPE in this example), and it creates a underlying file context and connects both.

Creating the instance in address space works the same way is ua_instance_new.

  1. Create a ua_filetype_ctx instance using ua_filetype_ctx_create, which is used to control the instance creation. Therefor you can register callbacks at this context (ua_filetype_ctx_set*cb), and you can set the file's MIME type using ua_filetype_ctx_set_mime_type.
  2. You call ua_filetype_create_ex and specify the file's new NodeId, its type and its implementation calltable. Depending on the used implementation you must specify additional data as the last argument. For the filesystem implementation this is the filename (const char*).
  3. You can get the node handle of the new instance by calling ua_filetype_ctx_get_new_node
  4. You cleanup the ua_filetype_ctx by calling ua_filetype_ctx_delete.
  5. You can add a reference to this node by using ua_reference_add.
  6. Optionally you can register callbacks to get notifications when a file is opened or closed. This may be useful if you need to do something after a file was updated.

Code snippet from the demoprovider:

/* create nodeid for file instance */
ret = ua_nodeid_attach_string_const(&id, g_uaprovider_demo_dynamic_nsidx, "testfile");
if (ret != UA_EGOOD) goto error;
/* create filetype context for ua_filetype_create_ex invocation */
if (ctx == NULL) goto error;
/* optionally, set the file's MIME type */
ua_filetype_ctx_set_mime_type(ctx, "application/text");
/* Create the file instance. */
ret = ua_filetype_create_ex(ctx, &id, UA_NODE_FILETYPE, 0, NULL, "Testfile", &ua_filesystem_implementation, UA_DEMOPROVIDER_TESTFILE);
if (ret != UA_EGOOD) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Creation of filetype for Testfile failed.\n");
goto error;
}
/* get the newly create node handle of the file */
/* cleanup */
/* reference node from existing folde node */
ref = ua_reference_add(folder_node, g_file, UA_NODE_ORGANIZES);
if (ref == UA_REF_INVALID) {
ret = UA_EBAD;
TRACE_ERROR(TRACE_FAC_PROVIDER, "Creation of reference to 014_Files folder failed.\n");
goto error;
}
/* register callbacks to get notifications for file changes */
ret = ua_filetype_set_callbacks(g_file, &cb);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "ua_filetype_set_callbacks failed.\n");
goto error;
}

Creating a Memory File Instance

Creating an in-memory file works essentially the same as the example above, with the following differences:

The following example uses ua_filetype_create instead of ua_filetype_create_ex to demonstrate this. This is just a convenience wrapper function, so that you don't need to create the ua_filetype_ctx yourself. This function is sufficient for most cases, but if you need control over the created NodeIds you must use ua_filetype_create_ex and register the according callbacks.

/* Create bytestring for in-memory file. */
ret = ua_bytestring_create(&g_memoryfiledata, 1024);
if (ret < 0) {
TRACE_ERROR(TRACE_FAC_PROVIDER, "Bytestring generation for memory file failed.\n");
goto error;
}
/* initialize bytestring with zeros */
ua_bytestring_clear_data(&g_memoryfiledata);
/* add some test data */
ua_memcpy(g_memoryfiledata.data, "Hello World", 11);
/* Set the string node id for "memoryfile". */
ret = ua_nodeid_attach_string_const(&id, g_uaprovider_demo_dynamic_nsidx, "memoryfile");
if (ret != UA_EGOOD) {
goto error;
}
/* Create the memory file. */
g_memoryfile = ua_filetype_create(&id, UA_NODE_FILETYPE, "Memoryfile", &ua_memoryfile_implementation, &g_memoryfiledata);
if (g_memoryfile == UA_NODE_INVALID) {
ret = UA_EBAD;
TRACE_ERROR(TRACE_FAC_PROVIDER, "Creation memory file failed.\n");
goto error;
}
/* cleanup */
ref = ua_reference_add(folder_node, g_memoryfile, UA_NODE_ORGANIZES);
if (ref == UA_REF_INVALID) {
ret = UA_EBAD;
TRACE_ERROR(TRACE_FAC_PROVIDER, "Creation of reference to 014_Files folder failed.\n");
goto error;
}
ret = ua_filetype_set_callbacks(g_memoryfile, &cb);

Adding new File Implementations

If you need other kinds of files you can add your own implementation of the file interface. All you need to do is creating an instance of ua_filetype_interface and set the function pointers of your implementation. Then use this calltable in ua_filetype_create like for the examples above.

The args argument of ua_filetype_create_ex will be forwarded to the create function of ua_filetype_interface. This way you can pass implementation specific data to your implementation. This private implementation details can be stored in the pimpl field of the ua_file_context. So this is available in all the function calls.

To understand this it's best to look at the existing two implementations which are shown in the UML diagrams below.

filetype_filesystem.png
FileSystem Model
filetype_memoryfile.png
MemoryFile Model