xentara-plugin v1.2.1
The Xentara Plugin Framework
Loading...
Searching...
No Matches
Publishing Attributes
See also
Attributes in the Xentara user manual

Xentara plugin elements can publish certain data as attributes, which can then be read and written as described in Reading and Writing Attributes.

In order to publish an attribute, an element must do the following things:

  1. The attribute's identity must be published using the resolveAttribute() callback.

For readable attributes, the element must do the following additional things:

  1. The attribute's value must be stored in a xentara::memory::MemoryResource.
  2. A read handle for the attribute must be made available using the readHandle() callback

For writable attributes, the element must do the following additional things:

  1. The attribute's value must be stored in a xentara::memory::MemoryResource, or a callback must be provided to write the value.
  2. A write handle for the attribute must be made available using the writeHandle() callback

I/O Attributes

See also
I/O Points
Data Points in the Xentara user manual

Some attributes of I/O points are used directly to perform the associated I/O operations, and to report the status and result of the I/O. These include the following standard attributes:

Each of these attributes is marked as an I/O attribute for the relevant data directions (input and/or output). If a data point uses a particular I/O point as input and/or output, then it inherits the relevant I/O attributes from the I/O point. A data point that uses an input that publishes the sourceTime attribute, for example, will automatically gain a sourceTime attribute of its own, that contains the same value as that of the input I/O point.

You can mark attributes you define yourself as input and/or output attributes by passing the relevant data directions to the constructor of xentara::model::Attribute. The Xentara OPC UA driver, for example, publishes the OPC UA serverTimestamp parameter asttribute named serverTime. This attribute is marked as an I/O attribute for the input direction, and this inherited by data points that use an OPC UA I/O point as an input.

Memory Resources

The values of attributes must be stored in a special memory resource. Currently, Xentara provides two memory resources:

Memory Blocks

Using Memory Blocks

In order to store data in a memory resource, a memory block must be allocated. Xentara can allocate three different types of memory blocks:

  1. A raw memory block holds raw data as a block accessed using by a void *
  2. A object memory block holds a single C++ object. To store multiple objects in a single object memory block, group them together into a C++ struct.
  3. A array memory block holds a heterogeneous array of C++ objects. The exact number and types of the objects can be defined at runtime, but must be known before the block is allocated.

All memory blocks are accessed using the template class xentara::memory::MemoryBlock. This template class takes two parameters, one is the type of memory resource to use, and the second is the structure object. The structure object determines what exactly the memory block will hold.

Memory block objects inherit from xentara::utils::tools::Unique, so they can be move constructed and move assigned, but not copy constructed or copy assigned. xentara::memory::MemoryBlock behaves like a smart pointer and automatically deallocates the memory block when necessary (e.g. in the descructor).

Raw Memory Blocks

Raw memory blocks are managed using a xentara::memory::MemoryBlock with the dummy class xentara::memory::RawMemory as structure. xentara::memory::RawMemory is the default parameter for the structure object, so that a raw memory block can simple be written as xentara::memory::MemoryBlock.

A raw memory block can be allocated in a memory resource using create(), and deallocated using destroy().

Object Memory Blocks

Object memory blocks are managed using a xentara::memory::ObjectBlock, which is a xentara::memory::MemoryBlock with the dummy template class xentara::memory::Object as structure. xentara::memory::ObjectBlock and xentara::memory::Object both take the C++ type of the contained object as template parameter.

An object memory block containing an object can be allocated in a memory resource using create(), and deallocated using destroy().

Array Memory Blocks

Array memory blocks are managed using a xentara::memory::ArrayBlock, which is a xentara::memory::MemoryBlock with xentara::memory::Array as structure. Contrary to xentara::memory::RawMemory and xentara::memory::Object, xentara::memory::Array is not a dummy class. You must provide an object of type xentara::memory::Array, and use it to define the internal structure of the array.

An array memory block takes the array that describes its structure as a parameter in its constructor. The array object need not contain the correct structure yet, the constructor merely binds the array object to the memory block. To construct a class containing an array memory block, you would usually proceed as follows:

class MyClass
{
private:
// The object containing the internal structure of the array block
// The actual array block
xentara::memory::ArrayBlock _arrayBlock { _structure };
};
A helper class used to describe structure of memory containing a heterogenious array.
Definition Array.hpp:22
A memory block containing an array of elements.
Definition ArrayBlock.hpp:31

To define the internal structure of the memory block, you must add elements to the xentara::memory::Array object using appendElement(), appendObject(), or appendObjects(). Each of these functions will return a handle to the newly added element or elements, that can then be used when reading or writing the data.

You can define the structure at any time before allocating the array. For memory blocks that hold value or status attributes, the callback realize() is a good place to define the structure of the array. In realize(), the entire model has already been loaded and the configuration of all objects is known, but no attribute read handles have been resolved yet. (Read handles are usually resolved in prepare(), which is called after realize().)

Here is an example of an I/O batch that allocates storage for all its I/O points:

auto MyIoBatch::realize()
{
// Add our I/O independent data
_dataHandle = _structure.appendObject<MyIoBatch::Data>();
// Add data for all the I/O points
for (auto &&ioPoint : _ioPoints)
{
ioPoint.setDataHandle(_structure.appendObject<MyIoPoint::Data>());
}
// Create the array
_arrayBlock.create();
};

An array memory block containing all the element added to the structure object can be allocated in a memory resource using create(), and deallocated using destroy().

Writing Memory Blocks

Write Sentinels

In order to write to a memory block, you need to have access to the block object.

The data in a memory block is always written atomically using swap-in: A new memory region is allocated for the new data, and then swapped in atomically to replace the old data on commit. The new data can either be default initialized, or copied from the old data. If the data is default initialized, all old values of all objects contained in the block will be lost, and the entire block must be rewritten from scratch. If the old data is copied, only the changed data need be written.

Writing to a memory block is done using a write sentinel. A write sentinel is an object of a template instantiation of xentara::model::WriteSentinel. The write sentinel object takes the memory resource type and the memory block type as template parameters.

To write to a block, four things need to be done:

  1. A write sentinel must be constructed,
  2. A reference to the C++ object to be written must be obtained,
  3. The new values must be written using the obtained reference,
  4. The new values must be committed.

Steps 2 and 3 can be combined into a single C++ statement.

Writing Raw Data Blocks

The data in a raw data block is accessed using a void *. To write a raw data block, proceed as follows:

// Create a write sentinel
xentara::memory::WriteSentinel sentinel { _block };
// Get a pointer to the data
void *data = sentinel->placement();
// Write the data
std::memcpy(data, whereverTheDataComesFrom, _dataSize);
// Commit the new values
sentinel.commit();
A sentinel that protects write access to memory in a memory resource.
Definition WriteSentinel_forward.hpp:14
T memcpy(T... args)

Or, simplified:

// Create a write sentinel
xentara::memory::WriteSentinel sentinel { _block };
// Write the data
std::memcpy(sentinel->placement(), whereverTheDataComesFrom, _dataSize);
// Commit the new values
sentinel.commit();

Replace the std::memcpy call with whatever is needed to write the data.

Please note that the new values are left uninitialized, so every byte of the new data must be written before calling commit().

You can access the old values using oldData().

Writing Object Data Blocks

The value in an object block is accessed using a reference to the C++ object. To write an object block, proceed as follows:

// Create a write sentinel
xentara::memory::WriteSentinel sentinel { _block };
// Get a reference to the value
auto &value = *sentinel;
// Set the value
value = whateverTheNewValueShouldBe;
// Commit the new value
sentinel.commit();

Or, simplified:

// Create a write sentinel
xentara::memory::WriteSentinel sentinel { _block };
// Set the value
*sentinel = whateverTheNewValueShouldBe;
// Commit the new value
sentinel.commit();

In this example, the new value is default initialized. If the value has multiple members, you must set all the members to the correct values, since no data is copied from the original value.

You can access the old value using oldValue().

You can also opt to copy the old value into the new value when creating the write sentinel. To accomplish this, pass the dummy object xentara::memory::kCopyOldValues to the [constructor](xentara::memory::workaround::ObjectWriteSentinel<Type>::WriteSentinel(const xentara::memory::ObjectBlock<Type> &, xentara::memory::CopyOldValuesTag). This will initialize the new value with the old one:

// Create a write sentinel, copying the old value
xentara::memory::WriteSentinel sentinel { _block, xentara::memory::kCopyOldValues };
// Get a reference to the value
auto &value = *sentinel;
// Modify only a part of the value
value.setSomeMember(whateverTheNewValueShouldBe);
// Commit the modified value
sentinel.commit();

Or, simplified:

// Create a write sentinel, copying the old value
xentara::memory::WriteSentinel sentinel { _block, xentara::memory::kCopyOldValues };
// Modify only a part of the value
sentinel->setSomeMember(whateverTheNewValueShouldBe);
// Commit the modified value
sentinel.commit();

Writing Array Data Blocks

The elements in an array block are accessed using the handles returned by xentara::memory::array::appendElement(), xentara::memory::array::appendObject(), and xentara::memory::array::appendObjects() when registering the elements with the structure object. To write an array data block, proceed as follows:

// Create a write sentinel
xentara::memory::WriteSentinel sentinel { _block };
// Get references to the elements
auto &element1 = sentinel[_element1Handle];
auto &element2 = sentinel[_element2Handle];
auto &element3 = sentinel[_element3Handle];
// Set the elements' values
element1 = whateverTheNewValueForElement1ShouldBe;
element2 = whateverTheNewValueForElement2ShouldBe;
element3 = whateverTheNewValueForElement3ShouldBe;
// Commit the new value
sentinel.commit();

Or, simplified:

// Create a write sentinel
xentara::memory::WriteSentinel sentinel { _block };
// Set the elements' values
sentinel[_element1Handle] = whateverTheNewValueForElement1ShouldBe;
sentinel[_element2Handle] = whateverTheNewValueForElement2ShouldBe;
sentinel[_element3Handle] = whateverTheNewValueForElement3ShouldBe;
// Commit the new value
sentinel.commit();

You can intermix the calls to operator[]() and the modification of the elements as you please. It is not necessary to first resolve all the references and then write all the values, any order is ok.

In this example, the values of the new elements are default initialized. You must set all the members of all elements to the correct values, since no data is copied from the original elements.

You can access the old elements using oldValues().

You can also opt to copy the old values of the elements into the new elements when creating the write sentinel. To accomplish this, pass the dummy object xentara::memory::kCopyOldValues to the constructor. This will initialize the new elements with the old ones:

// Create a write sentinel, copying the old value
xentara::memory::WriteSentinel sentinel { _block, xentara::memory::kCopyOldValues };
// Get a reference to a single element
auto &someRandomElement = sentinel[_someRandomElementHandle];
// Modify only that one element
someRandomElement = whateverTheNewValueShouldBe + 'x';
// Commit the modified element
sentinel.commit();

Or, simplified:

// Create a write sentinel, copying the old value
xentara::memory::WriteSentinel sentinel { _block, xentara::memory::kCopyOldValues };
// Modify only a single element
sentinel[_someRandomElementHandle] = whateverTheNewValueShouldBe + 'x';
// Commit the modified element
sentinel.commit();

Reading Memory Blocks

Read Sentinels

In order to read from a memory block, you need to have access to the block object.

Reading from a memory block is done using a read sentinel. A read sentinel is an object of a template instantiation of xentara::model::ReadSentinel. The read sentinel object takes the memory resource type and the memory block type as template parameters.

To read to a block, three things need to be done:

  1. A read sentinel must be constructed,
  2. A reference to the C++ object to be read must be obtained,
  3. The values must be read using the obtained reference

The last two steps can be combined into a single C++ statement.

Reading Raw Data Blocks

The data in a raw data block is accessed using a const void *. To read a raw data block, proceed as follows:

// Create a read sentinel
xentara::memory::ReadSentinel sentinel { _block };
// Get a pointer to the data
const void *data = sentinel->placement();
// Read the data
std::memcpy(whereverTheDataShouldGo, data, _dataSize);
A sentinel that protects read access to memory in a memory resource.
Definition ReadSentinel_forward.hpp:18

Or, simplified:

// Create a read sentinel
xentara::memory::ReadSentinel sentinel { _block };
// Read the data
std::memcpy(whereverTheDataShouldGo, sentinel->placement(), _dataSize);

Replace the std::memcpy call with whatever is needed to read the data.

Reading Object Data Blocks

The value in an object block is accessed using a reference to the C++ object. To read an object block, proceed as follows:

// Create a read sentinel
xentara::memory::ReadSentinel sentinel { _block };
// Get a reference to the value
const auto &value = *sentinel;
// Store the value
whereverTheValueShouldGo = value;

Or, simplified:

// Create a read sentinel
xentara::memory::ReadSentinel sentinel { _block };
// Read the value
whereverTheValueShouldGo = *sentinel;

You do not have to copy the entire value, of course. You can access individual members, call const member functions, pass the object on to another function, or do anything you can normally do with a C++ reference. Here is an example of reading a single member of a class object:

// Create a read sentinel
xentara::memory::ReadSentinel sentinel { _block };
// Get a reference to the value
const auto &value = *sentinel;
// Read only a single member of the value
whereverTheValueShouldGo = value.someMember();

Or, simplified:

// Create a read sentinel
xentara::memory::ReadSentinel sentinel { _block };
// Read only a single member of the value
whereverTheValueShouldGo = sentinel->someMember();

Reading Array Data Blocks

The elements in an array block are accessed using the handles returned by xentara::memory::array::appendElement(), xentara::memory::array::appendObject(), and xentara::memory::array::appendObjects() when registering the elements with the structure object. To read an array data block, proceed as follows:

// Create a read sentinel
xentara::memory::ReadSentinel sentinel { _block };
// Get references to the elements
const auto &element1 = sentinel[_element1Handle];
const auto &element2 = sentinel[_element2Handle];
const auto &element3 = sentinel[_element3Handle];
// Set the elements' values
element1 = whateverTheNewValueForElement1ShouldBe;
element2 = whateverTheNewValueForElement2ShouldBe;
element3 = whateverTheNewValueForElement3ShouldBe;

Or, simplified:

// Create a read sentinel
xentara::memory::ReadSentinel sentinel { _block };
// Set the elements' values
sentinel[_element1Handle] = whateverTheNewValueForElement1ShouldBe;
sentinel[_element2Handle] = whateverTheNewValueForElement2ShouldBe;
sentinel[_element3Handle] = whateverTheNewValueForElement3ShouldBe;

You can intermix the calls to operator[]() and the modification of the elements as you please. It is not necessary to first resolve all the references and then read all the values, any order is ok.

Read Handles

Write Handles