COMMS
Template library intended to help with implementation of communication protocols.
Fields Definition Tutorial

Fields are abstractions around value storage primitives and/or objects, such as integral values, floating point values, strings, arrays, etc.. Every field class is defined in comms::field namespace and exposes predefined interface in order to make template meta-programming as easy as possible. As an example let's take a look at comms::field::IntValue class which is used to define integral value field.

template <typename TBase, typename T, typename... TOptions>
class comms::field::IntValue : public TBase
{
public:
// Define inner storage type
using ValueType = T;
// Type used for version update
using VersionType = typename TBase::VersionType;
// Get access to the stored value
ValueType& value() { return m_value; }
const ValueType& value() const { return m_value; }
// Read
template <typename TIter>
comms::ErrorStatus read(TIter& iter, std::size_t len) {...}
// Write
template <typename TIter>
comms::ErrorStatus write(TIter& iter, std::size_t len) const {...}
// Serialisation length
std::size_t length() const {...}
// Validity of the value
bool valid() const {...}
// Bring field's contents into a consistent state
bool refresh() {...}
// Update protocol version
// Compile time check whether the field's contents may change as the
// result of protocol version update
static constexpr bool isVersionDependent() {...};
private:
ValueType m_value;
}
Field that represent integral value.
Definition: IntValue.h:72
typename BaseImpl::ValueType ValueType
Type of underlying integral value.
Definition: IntValue.h:93
typename BaseImpl::VersionType VersionType
Version type.
Definition: IntValue.h:83
bool setVersion(VersionType version)
Default implementation of version update.
Definition: IntValue.h:384
bool valid() const
Check validity of the field value.
Definition: IntValue.h:287
const ValueType & value() const
Get access to integral value storage.
Definition: IntValue.h:227
constexpr std::size_t length() const
Get length required to serialise the current field value.
Definition: IntValue.h:267
ErrorStatus read(TIter &iter, std::size_t size)
Read field value from input data sequence.
Definition: IntValue.h:305
ErrorStatus write(TIter &iter, std::size_t size) const
Write current field value to output data sequence.
Definition: IntValue.h:340
static constexpr bool isVersionDependent()
Compile time check if this class is version dependent.
Definition: IntValue.h:364
bool refresh()
Refresh the field's value.
Definition: IntValue.h:294
comms::option::def::VersionType< T > VersionType
Same as comms::option::def::VersionType.
Definition: options.h:1797
ErrorStatus
Error statuses reported by the Communication module.
Definition: ErrorStatus.h:17

The main things to note are that every field definition class:

  • receives its base class as the first template parameter. It is expected to be a variant of comms::Field with comms::option::def::BigEndian or comms::option::def::LittleEndian option to specify the serialisation endian.
  • exhibits some default behaviour which can be modified by passing various options from comms::option namespace as additional template parameters. All the available options are described below in this tutorial.
  • defines ValueType inner value storage type and provides value() member functions to access the stored value.
  • provides read() and write() member functions to read and write the inner value given the iterator used for reading / writing and available length of the buffer.
  • has length() member function to report how many bytes are required to serialise currently stored value.
  • provides valid() member function to check whether the stored value is valid (within expected range of values).
  • has refresh() member function to bring its contents to consistent / valid state when required.
  • has setVersion() member function to notify field object that the the protocol version has changed.

Also note that all the member function are NON-virtual, i.e. the field abstractions do not have polymorphic behaviour.

The available fields abstractions are:

Integral Value Fields

Integral values are abstracted by comms::field::IntValue class, which receives at least two template parameters. The first one is a base class, from which the comms::field::IntValue will inherit. It must be a variant of comms::Field, with the option specifying endian used for data serialisation. The second template parameter is a basic integral type that is used to store the field's value.
For example:

Base class to all the field classes.
Definition: Field.h:33

The example above defines a field that uses std::int16_t type to store its value. The value can be accessed using value() member function:

MyIntField intField;
std::cout << "Default value: " << intField.value() << '\n'; // prints 0
intField.value() = 5;
std::cout << "Updated value: " << intField.value() << std::endl; // prints 5

When such field is serialised, 2 bytes (sizeof(std::int16_t)) are written to the output buffer, most significant first and less significant second (because MyFieldBase base class was defined using comms::option::def::BigEndian option).

Modifying Serialisation Length

Sometimes protocol specification tries to reduce amount of data transferred over I/O link. It may define serialisation length of the field that differs from standard length of basic integral types, such as std::int8_t, std::uint8_t, std::int16_t, std::uint16_t, std::int32_t, std::uint32_t, ... For example, some field may only have values between 0 and 10,0000,000, which may be encoded using only 3 bytes, and that's what the protocol specifies. The storage type for such value is going to be std::uint32_t, but there is a need to limit serialisation length for it. The COMMS library provides comms::option::def::FixedLength option, that can be used for this purpose.

Variable Serialisation Length

There are protocols, that try to reduce amount of traffic over I/O link by using variable length when serialising numeric value. Usually it is Base-128 encoding, where the most significant bit in the byte indicates whether it is the last byte in the numeric encoding or the next one also needs to be taken into account. The COMMS library provides comms::option::def::VarLength option that can be used with comms::field::IntValue and modifies the behaviour of the latter to expose the required read()/write()/length() behaviour:

// Variable length encoding, encoding takes at least 1 byte and at most 4 bytes.

The field's base class (MyFieldBase) contains endian information which is used to determine which part of the value is serialised first.

Serialisation Offset

There are cases when there is a need to add/subtract some predefined offset to/from the value of the field when serialisation takes place. Good example of such case would be serialising a "current year" value. Most protocols now specify it as an offset from year 2000 or later and serialised as a single byte, i.e. to specify year 2015 is to write value 15. However it may be inconvenient to manually adjust serialised/deserialised value by predefined offset 2000. To help with such case option comms::option::def::NumValueSerOffset can be used. For example:

using YearField =
MyFieldBase,
std::int16_t,
>;
static const std::uint8_t SerData[] = { 15 }; // Pretend serialisation data
static const std::size_t SerDataLen = std::extent<decltype(SerData)>::value;
YearField year;
auto* readIter = &SerData[0];
auto es = year.read(readIter, SerDataLen); // Read year information
assert(es == comms::ErrorStatus::Success); // No failure is expected
std::cout << year.value() << std::endl; // Prints 2015;
// Modify year value:
year.value() = 2016;
std::vector<std::uint8_t> outData; // Pretend output buffer
auto writeIter = std::back_inserter(outData);
es = year.write(writeIter, outData.max_size());
assert(es == comms::ErrorStatus::Success); // No failure is expected
assert(outData.size() == 1U); // Only 1 byte is expected to be pushed to outData,
// due to using comms::option::def::FixedLength<1> option.
assert(outData[0] == 16); // The value equal to "year.value() - 2000" is expected to be written.
@ Success
Used to indicate successful outcome of the operation.
Option used to specify number of bytes that is used for field serialisation.
Definition: options.h:280
Option to specify numeric value serialisation offset.
Definition: options.h:378

Scaling Value

Sometimes systems operate with floating point numbers. Let's say to handle the distance between two points on the map in meters. However, when communicating this information over the I/O link, the developers often scale the floating point value up in order to send such value as integer. For example, the distance is communicated in millimeters (when calculated and handled in meters). The definition of such field may look like:

using DistanceField =
MyFieldBase,
std::uint16_t,
>;
Option to specify scaling ratio.
Definition: options.h:410

The comms::option::def::ScalingRatio option allows scaling of serialised value (distance in mm) to handling value (distance in m) and vice versa:

static const std::uint8_t InData[] = {0x3, 0xe8}; // Pretend input buffer, encoded 1000
static const std::size_t InDataSize = std::extent<decltype(InData)>::value;
DistanceField dist;
const auto* readIter = &InData[0];;
auto es = dist.read(readIter, InDataSize);
assert(es == comms::ErrorStatus::Success); // No error is expected
std::cout << "Distance in mm: " << dist.value() << '\n'; // Prints 1000
std::cout << "Distance in m: " << dist.getScaled<float>() << std::endl; // Prints 1.0
dist.setScaled(2.3);
std::cout << "New distance in mm: " << dist.value() << std::endl; // Prints 2300

The scaling may work in the opposite direction of increasing the number. For example, the field contains number of tens of millimeters between two points. It would be convenient to be able to convert it to proper millimeters number. As the result the field can be defined as:

using OtherDistanceField =
MyFieldBase,
std::uint16_t,
>;

The comms::option::def::ScalingRatio option allows scaling of serialised value (distance in tens of mm) to handling value (distance in mm) and vice verse:

static const std::uint8_t InData[] = {0x0, 0xf}; // Pretend input buffer, encoded 15
static const std::size_t InDataSize = std::extent<decltype(InData)>::value;
OtherDistanceField dist;
const auto* readIter = &InData[0];;
auto es = dist.read(readIter, InDataSize);
assert(es == comms::ErrorStatus::Success); // No error is expected
std::cout << "Distance in tens of mm: " << dist.value() << '\n'; // Prints 15
std::cout << "Distance in mm: " << dist.getScaled<unisnged>() << std::endl; // Prints 150
dist.setScaled(500);
std::cout << "New distance in tens of mm: " << dist.value() << std::endl; // Prints 50

Methods comms::field::IntValue::getScaled and comms::field::IntValue::setScaled take into account scaling ratio provided (with comms::option::def::ScalingRatio option) to the comms::field::IntValue field. If such option wasn't used
comms::option::def::ScalingRatio<1, 1> is assumed.

Value Units

In addition to Scaling Value, the COMMS library provides an ability to specify field's value units and perform conversion between units of the same type. Let's get back to the same example of defining distance between two point, but instead of providing scaling ratio directly, the type of the units is specified.

using DistanceField =
MyFieldBase,
std::uint16_t,
>;
Options to specify units of the field.
Definition: options.h:693

The comms::option::def::UnitsMillimeters option specifies that field contains distance in millimeters, which allows COMMS library provide proper conversion to other distance units when necessary:

static const std::uint8_t InData[] = {0x3, 0xe8}; // Pretend input buffer, encoded 1000
static const std::size_t InDataSize = std::extent<decltype(InData)>::value;
DistanceField dist;
const auto* readIter = &InData[0];;
auto es = dist.read(readIter, InDataSize);
assert(es == comms::ErrorStatus::Success); // No error is expected
std::cout << "Original value: " << dist.value() << '\n'; // Prints 1000
std::cout << "Distance in mm: " << comms::units::getMillimeters<unsigned>(dist) << std::endl; // Prints 1000
std::cout << "Distance in cm: " << comms::units::getCentimeters<float>(dist) << std::endl; // Prints 100.0
std::cout << "Distance in m: " << comms::units::getMeters<float>(dist) << std::endl; // Prints 1.0
std::cout << "New value: " << dist.value() << '\n'; // Prints 55
std::cout << "New distance in mm: " << comms::units::getMillimeters<unsigned>(dist) << std::endl; // Prints 55
std::cout << "New distance in cm: " << comms::units::getCentimeters<float>(dist) << std::endl; // Prints 5.5
std::cout << "New distance in m: " << comms::units::getMeters<float>(dist) << std::endl; // Prints 0.055
void setCentimeters(TField &field, TVal &&val)
Update field's value accordingly, while providing centimeters value.
Definition: units.h:1125

In the examples above the "units" specification may replace the "scaling" information. However, there are cases when it they may complement each other. For example, the field contains "latitude" information in degrees but multiplied by 10'000'000 to make integral value out of floating point.

The COMMS library uses the scaling ratio as well as units information to be able to convert the stored value between degrees and radians when needed.

LatField lat(123456789); // Encoded latitude of 12.3456789
std::cout << "Raw value: " << lat.value() << std::endl; // Prints 123456789
std::cout << "Lat in degrees: " << comms::units::getDegrees<double>(lat) << std::endl; // Prints 12.3456789
std::cout << "Lat in radians: " << comms::units::getRadians<float>(lat) << std::endl; // 0.21547274519
std::cout << "New raw value: " << lat.value() << std::endl; // Prints 223300000
std::cout << "New degrees value: " << comms::units::getDegrees<double>(lat) << std::endl; // Prints 22.33
std::cout << "New radians value: " << comms::units::getRadians<double>(lat) << std::endl; // Prints 0.38973202
std::cout << "Updated raw value: " << lat.value() << std::endl; // Prints 600000000
std::cout << "Updated degrees value: " << comms::units::getDegrees<double>(lat) << std::endl; // Prints 60
std::cout << "Updated radians value: " << comms::units::getRadians<double>(lat) << std::endl; // Prints 1.04719
void setDegrees(TField &field, TVal &&val)
Update field's value accordingly, while providing degrees value.
Definition: units.h:1867
void setRadians(TField &field, TVal &&val)
Update field's value accordingly, while providing radians value.
Definition: units.h:1920

The COMMS library provides mulitple options to specify the units of the field's value:

All the units conversion functions reside in comms::units namespace. NOTE, that conversion can be applied only between the units of the same type. The units compitability check is performed at compile time and the compilation will fail on attempt to set/get incompatible value, such as setting/getting "seconds" to/from the field specified as containing millimeters.

The whole units conversion functionality can be useful in a client code, that requires usage of particular unit types in its internal calculations. It can use conversion functions without any need to know scaling ratio and/or actual units of the field, the COMMS library will do all the necessary math calculation to provide the requested value.

Other Options

There multiple common options that are applicable to all the fields, comms::field::IntValue included. Please refer to Common Options or Modifications for the Fields for more details.

Enum Value Fields

Sometimes it is more convenient to operate with enum types instead of integral values. For example, the custom protocol message carries information of how to configure some external serial port, and one of the values is the baud rate. In order not to impose too much overhead on I/O link, the protocol developers decided to use single byte to indicate one standard baud rate:

Baud Rate Serialisation Value
9600 0
14400 1
19200 2
28800 3
38400 4
57600 5
115200 6

It would be more convenient to define enum type to operate with, instead of using raw numbers.

enum Baud : std::uint8_t // The underlying type should be explicitly specified
{
Baud_9600,
Baud_14400,
Baud_19200,
Baud_28800,
Baud_38400,
Baud_57600,
Baud_115200
};
BaudField baud;
...
baud.value() = Baud_115200; // Set the value.
std::vector<std::uint8_t> outData; // Pretend output buffer
auto writeIter = std::back_inserter(outData);
auto es = baud.write(writeIter, outData.max_size());
assert(es == comms::ErrorStatus::Success); // No error is expected
assert(outData.size() == 1); // Single byte output is expected
assert(outData[0] == 6U); // Value 6 is expected to be written
Enumerator value field.
Definition: EnumValue.h:73
const ValueType & value() const
Get access to enum value storage.
Definition: EnumValue.h:149

comms::field::EnumValue is very similar to comms::field::IntValue. The main difference is using enum instead of integral type as a second template parameter. The default serialisation length is determined by the underlying type of the enum. That't why it is important to explicitly specify the underlying type of the enum when defining it, and not leave this to the compiler.

The comms::field::EnumValue field supports almost all the options that can be used with comms::field::IntValue: Modifying Serialisation Length, Variable Serialisation Length, Serialisation Offset, as well as Common Options or Modifications for the Fields.

Bitmask Value Fields

Quite often messages in communication protocol use some kind of flags, where single bit has a independent meaning. It is more convenient to treat such flags as bitmasks rather than integral values. comms::field::BitmaskValue provides a convenient interface to handle such bitmasks.

Bitmask value field.
Definition: BitmaskValue.h:103

By default the underlying storage type of the comms::field::BitmaskValue is unsigned, which makes the default serialisation length to be sizeof(unsigned). The modification of the underlying storage type as well as serialisation length can be done using comms::option::def::FixedLength option (see Modifying Serialisation Length). The underlying type will always be some unsigned integral type. If the serialisation length is specified to be 1 byte, the underlying storage type is std::uint8_t, if the serialisation length is 2 bytes, the underlying storage type is std::uint16_t, if the serialisation length is 3 or 4 bytes, the underlying storage type is std::uin32_t, etc...

static_assert(std::is_same<BitmaskField_1byte::ValueType, std::uint8_t>::value, "std::uint8_t type is expected");
assert(BitmaskField_1byte().length() == 1U);
static_assert(std::is_same<BitmaskField_2bytes::ValueType, std::uint16_t>::value, "std::uint16_t type is expected");
assert(BitmaskField_2bytes().length() == 2U);
static_assert(std::is_same<BitmaskField_3bytes::ValueType, std::uint32_t>::value, "std::uint32_t type is expected");
assert(BitmaskField_2bytes().length() == 3U);
static_assert(std::is_same<BitmaskField_4bytes::ValueType, std::uint32_t>::value, "std::uint32_t type is expected");
assert(BitmaskField_2bytes().length() == 4U);

All the Common Options or Modifications for the Fields can also be used with comms::field::BitmaskValue.

Reserved Bits

Quite often the bitmask fields contain reserved bits, which must preserve some values (usually 0). The comms::field::BitmaskValue fields support usage of comms::option::def::BitmaskReservedBits alias option. The template parameters of the option specify mask for reserved bits as well as their expected values. The check for the reserved bits values is performed inside comms::field::BitmaskValue::valid() member function.

using MyBitmask =
MyFieldBase,
comms::option::def::BitmaskReservedBits<0x2, 0> // Second bit is reserved and must be 0
>;
Option that specifies custom validation class.
Definition: options.h:662

Bit Names

Quite often there is a need to provide names for the bits in the comms::field::BitmaskValue field. It is possible to define it as external independent enum. However, it may be convenient to define it as internal type. It is possible to do by inheriting from appropriate comms::field::BitmaskValue type and use COMMS_BITMASK_BITS() macro to define names for bits. For example

struct MyBitmask : public
MyFieldBase,
comms::option::def::FixedLength<1>,
comms::option::def::BitmaskReservedBits<0x2, 0> // Second bit is reserved and must be 0
>
{
COMMS_BITMASK_BITS(first, third=2, fourth, fifth, sixth, seventh, eighth);
}
#define COMMS_BITMASK_BITS(...)
Provide names for bits in comms::field::BitmaskValue field.
Definition: BitmaskValue.h:590

is equivalent to defining:

struct MyBitmask : public
MyFieldBase,
comms::option::def::FixedLength<1>,
comms::option::def::BitmaskReservedBits<0x2, 0>
>
{
enum BitIdx
{
BitIdx_first,
BitIdx_third=2,
BitIdx_fourth,
BitIdx_fifth,
BitIdx_sixth,
BitIdx_seventh,
BitIdx_eighth,
BitIdx_numOfValues,
}
}

NOTE, that provided names have found their way to BitIdx enum type, and got prefixed with BitIdx_. This indices may be used with comms::field::BitmaskValue::getBitValue() and comms::field::BitmaskValue::setBitValue() member functions.

Also note, that there is automatically generated BitIdx_numOfValues value to indicate end of the names list.

Due to the fact that the provided bit names may have =val suffixes, it excludes the ability to generate proper access functions for the named bits. However, the COMMS library also provides COMMS_BITMASK_BITS_ACCESS() macro, which can be used in addition to COMMS_BITMASK_BITS() one to generate the convenience functions. For example:

struct MyBitmask : public
MyFieldBase,
comms::option::def::FixedLength<1>,
comms::option::def::BitmaskReservedBits<0x2, 0> // Second bit is reserved and must be 0
>
{
COMMS_BITMASK_BITS(first, third=2, fourth, fifth, sixth, seventh, eighth);
COMMS_BITMASK_BITS_ACCESS(first, third, fourth, fifth, sixth, seventh, eighth);
}
#define COMMS_BITMASK_BITS_ACCESS(...)
Generate access functions for bits in comms::field::BitmaskValue field.
Definition: BitmaskValue.h:684

is equivalent to defining:

struct MyBitmask : public
MyFieldBase,
comms::option::def::FixedLength<1>,
comms::option::def::BitmaskReservedBits<0x2, 0>
>
{
enum BitIdx {...}
bool getBitValue_first() const { return getBitValue(BitIdx_first); }
void setBitValue_first(bool val) { setBitValue(BitIdx_first, val); }
bool getBitValue_third() const { ... }
void setBitValue_third(bool val) { ... }
bool getBitValue_fourth() const { ... }
void setBitValue_fourth(bool val) { ... }
...
}
bool getBitValue(unsigned bitNum) const
Get bit value.
Definition: BitmaskValue.h:349
void setBitValue(unsigned bitNum, bool val)
Set bit value.
Definition: BitmaskValue.h:356

In case the bit names definition is sequential starting with index 0 and going up without and gaps, i.e. no =val suffixes are used, the usage of two separate COMMS_BITMASK_BITS() and COMMS_BITMASK_BITS_ACCESS() macros, can be unified into one COMMS_BITMASK_BITS_SEQ():

struct MyBitmask : public
MyFieldBase,
comms::option::def::FixedLength<1>,
comms::option::def::BitmaskReservedBits<0xf0, 0> // 4 MSBs are reserved
>
{
COMMS_BITMASK_BITS_SEQ(first, second, third, fourth);
}
#define COMMS_BITMASK_BITS_SEQ(...)
Combine usage of COMMS_BITMASK_BITS() and COMMS_BITMASK_BITS_ACCESS().
Definition: BitmaskValue.h:776

WARNING: Some compilers, such as clang or earlier versions of gcc (v4.9 and earlier) may have problems compiling the COMMS_BITMASK_BITS_ACCESS() and COMMS_BITMASK_BITS_SEQ() macros even though they contain valid C++11 code. If the compilation failure happens and the bitmask definition class is NOT a template one (like in the example above), then try to substitute the used macros with COMMS_BITMASK_BITS_ACCESS_NOTEMPLATE() and COMMS_BITMASK_BITS_SEQ_NOTEMPLATE() respectively. For example:

struct MyBitmask : public
MyFieldBase,
comms::option::def::FixedLength<1>,
comms::option::def::BitmaskReservedBits<0xf0, 0> // 4 MSBs are reserved
>
{
COMMS_BITMASK_BITS_SEQ_NOTEMPLATE(first, second, third, fourth);
}
#define COMMS_BITMASK_BITS_SEQ_NOTEMPLATE(...)
Similar to COMMS_BITMASK_BITS_SEQ(), but dedicated for non-template classes.
Definition: BitmaskValue.h:791

However, when the defined bitmask class is a template, then the inner definition of Base type, which specifies the exact type of the base class, is required. For example:

template <typename... TExtraOptions>
class MyBitmask : public
MyFieldBase,
comms::option::def::FixedLength<1>,
comms::option::def::BitmaskReservedBits<0xf0, 0>, // 4 MSBs are reserved
TExtraOptions...
>
{
// Duplicate base class type
using Base =
MyFieldBase,
TExtraOptions...
>;
public:
COMMS_BITMASK_BITS_SEQ(first, second, third, fourth);
}

The same goes for COMMS_BITMASK_BITS_ACCESS() macro.

NOTE, that COMMS library also defines COMMS_MUST_DEFINE_BASE in case the base class definition is needed (going to be used). If the developed application is going to be multi-platform and compiled with various compilers (some of which may warn about unused private type) it is possible to use the defined symbol to add / remove the definition of the Base member type.

template <typename... TExtraOptions>
class MyBitmask : public comms::field::BitmaskValue<...>
{
#ifdef COMMS_MUST_DEFINE_BASE
using Base = ...;
#endif
public:
COMMS_BITMASK_BITS_SEQ(first, second, third, fourth);
}

Bitfield Fields

Many communication protocols try to pack multiple independent values into a one or several bytes to save traffic on I/O link. For example, to encode baud rate from example in Enum Value Fields section, only 3 bits are needed (values [0 - 6]). The serial port configuration may also require parity information, which may have only "None", "Even", and "Odd" values:

Parity Serialisation Value
None 0
Odd 1
Even 2
enum Parity : std::uint8_t
{
Parity_None,
Parity_Odd,
Parity_Even
};

To encode parity value only 2 bits are needed. Together with the baud mentioned earlier, these two values will consume only 5 bits. Let's also use the remaining 3 bits to complete a single byte as some kind of flags.

Value Number of bits
Baud 3
Parity 2
Flags 3

These value must be accessed and treated as independent values. However, they must be bundled into a single byte when serialisation happens. The COMMS library provides comms::field::Bitfield field for this purpose.

Please pay attention to the following details:

Every member of the bitfield may use all the supported options. The comms::field::Bitfield itself may receive only options listed in its class description.

To get an access to the member fields use value() member function:

SerialConfigField serialConfigField;
...
auto& members = serialConfigField.value(); // Reference to the stored tuple of field members
auto& buadField = std::get<0>(members); // Reference to the baud field;
auto& parityField = std::get<1>(members); // Reference to the parity field;
auto& flagsField = std::get<2>(members); // Reference to the flags field
baudField.value() = Baud_115200; // =6
parityField.value() = Parity_Even; // =2
flagsField.value() = 0x2;
std::vector<std::uint8_t> outData; // Pretend output buffer
auto writeIter = std::back_inserter(outData);
auto es = baud.write(writeIter, outData.max_size());
assert(es == comms::ErrorStatus::Success); // No error is expected
assert(outData.size() == 1); // Single byte output is expected
assert(outData[0] == 0x56); // Binary value split to 3-2-3 bits: 010|10|110

It would be convenient to access the member fields by name, rather than by index with std::get. It can be achieved by using COMMS_FIELD_MEMBERS_NAMES() macro inside field definition class.

class SerialConfigField : public comms::field::Bitfield<...>
{
// (Re)definition of the base class as inner Base type is
// the requirement of @ref COMMS_FIELD_MEMBERS_NAMES() macro
using Base = comms::field::Bitfield<...>
public:
COMMS_FIELD_MEMBERS_NAMES(baud, parity, flags);
}
#define COMMS_FIELD_MEMBERS_NAMES(...)
Provide names for member fields of composite fields, such as comms::field::Bundle or comms::field::Bi...
Definition: Field.h:380

It is equivalent to having the following enum, types and functions defined:

class SerialConfigField : public comms::field::Bitfield<...>
{
public:
// Access indices for member fields
enum FieldIdx {
FieldIdx_baud,
FieldIdx_parity,
FieldIdx_flags,
FieldIdx_numOfValues
};
// Accessor to "baud" field
auto field_baud() -> decltype(std::get<FieldIdx_baud>(value()))
{
return std::get<FieldIdx_baud>(value());
}
// Accessor to const "baud" field
auto field_baud() const -> decltype(std::get<FieldIdx_baud>(value()))
{
return std::get<FieldIdx_baud>(value());
}
// Accessor to "parity" field
auto field_parity() -> decltype(std::get<FieldIdx_parity>(value()))
{
return std::get<FieldIdx_parity>(value());
}
// Accessor to const "parity" field
auto field_parity() const -> decltype(std::get<FieldIdx_parity>(value()))
{
return std::get<FieldIdx_parity>(value());
}
// Accessor to "flags" field
auto field_flags() -> decltype(std::get<FieldIdx_flags>(value()))
{
return std::get<FieldIdx_flags>(value());
}
// Accessor to const "flags" field
auto field_flags() const -> decltype(std::get<FieldIdx_flags>(value()))
{
return std::get<FieldIdx_flags>(value());
}
// Redefinition of the members fields types:
using Field_baud = ...;
using Field_parity = ...;
using Field_flags = ...;
};
const ValueType & value() const
Get access to the stored tuple of fields.
Definition: Bitfield.h:194

NOTE, that provided names baud, parity, and flags, have found their way to the following definitions:

  • FieldIdx enum. The names are prefixed with FieldIdx_. The FieldIdx_nameOfValues value is automatically added at the end.
  • Accessor functions prefixed with field_
  • Types of member fields prefixed with Field_*

As the result, the fields can be accessed using multiple ways: For example using FieldIdx enum

SerialConfigField field;
auto& members = field.value(); // get access to the std::tuple of member fields
auto& baudField = std::get<SerialConfigField::FieldIdx_baud>(members);
auto& parityField = std::get<SerialConfigField::FieldIdx_parity>(members);
auto& flagsField = std::get<SerialConfigField::FieldIdx_flags>(members);
auto baud = baudField.value();
auto parity = parityField.value();
auto flags = flagsField.value();

or using accessor functions:

SerialConfigField field;
auto baud = field.field_baud().value();
auto parity = field.field_parity().value();
auto flags = field.flags.value();

SIDE NOTE: In addition to COMMS_FIELD_MEMBERS_NAMES() macro there is COMMS_FIELD_MEMBERS_ACCESS() one. It is very similar to COMMS_FIELD_MEMBERS_NAMES() but does NOT (re)define the inner Field_* types. It also does not require (except for clang) having base class to be (re)defined as inner Base type.

class SerialConfigField : public comms::field::Bitfield<...>
{
public:
COMMS_FIELD_MEMBERS_ACCESS(baud, parity, flags);
}
#define COMMS_FIELD_MEMBERS_ACCESS(...)
Add convenience access enum and functions to the members of composite fields, such as comms::field::B...
Definition: Field.h:236

In fact COMMS_FIELD_MEMBERS_NAMES() is implemented as the wrapper around COMMS_FIELD_MEMBERS_ACCESS().

Bundle Fields

There are cases when multiple independent fields need to be bundled into a single field and expose the required interface of reading, writing, calculating length, checking field's contents validity, and bringing field's value into a consistent state. It may be required when a message contains sequence (see Array List Fields) of such bundles/structs. The COMMS library provides comms::field::Bundle field for this purpose. It is quite similar to comms::field::Bitfield described earlier. The difference is that every member field doesn't specify any length in bits, just bytes. For example:

enum SomeEnum : std::uint8_t
{
SomeEnum_Value1,
SomeEnum_Value2,
SomeEnum_Value3,
...
}
using MyBundle =
MyFieldBase,
std::tuple<
>
>;
MyBundle bundleField;
...
auto& members = bundleField.value(); // Reference to the stored tuple of field members
auto& intValueField = std::get<0>(members);
auto& enumValueField = std::get<1>(members);
auto& bitmaskValueField = std::get<2>(members);
intValueField.value() = ...; // access the value of IntValue member field.
enumValueField.value() = ...; // access the value of EnumValue member field.
bitmaskValueField.value() = ...; // access the value of BitmaskValue member field.
std::vector<std::uint8_t> outData; // Pretend output buffer
auto writeIter = std::back_inserter(outData);
auto es = baud.write(writeIter, outData.max_size());
assert(es == comms::ErrorStatus::Success); // No error is expected
assert(outData.size() == 4); // Expected 2 bytes for IntValue, 1 byte for EnumValue and 1 byte for BitmaskValue
const ValueType & value() const
Get access to underlying mask value storage.
Definition: BitmaskValue.h:193
Bundles multiple fields into a single field.
Definition: Bundle.h:61

The default behaviour of comms::field::Bundle may be extended with options. Please refer to the class documentation for the list of supported options.

Just like with the Bitfield Fields, the names to the member fields can be provided by using COMMS_FIELD_MEMBERS_NAMES() or COMMS_FIELD_MEMBERS_ACCESS() macro.

class MyBundle : public comms::field::Bundle<...>
{
// (Re)definition of the base class as inner Base type is
// required by COMMS_FIELD_MEMBERS_NAMES() macro.
using Base = comms::field::Bundle<...>;
public:
COMMS_FIELD_MEMBERS_NAMES(member1, member2, member3);
};

It will create similar enum and convenience access functions, just like described in previous Bitfield Fields section.

Some bundle fields in some protocols may contain a field, which holds a remaining serialization length of the following member fields in the bundle. The COMMS library has a built-in support for such cases, it requires usage of comms::option::def::RemLengthMemberField option to specify index of such field. For example:

template <typename... TExtraOptions>
class MyBundle : public
MyFieldBase,
std::tuple<
comms::field::IntValue<FieldBase, std::uint8_t>, // remaing length info
SomeField1,
SomeField2,
SomeField3
>,
comms::option::def::RemLengthMemberField<0>, // Index of the remaining length field is 0
TExtraOptions...
>
{
using Base = ...;
public:
COMMS_FIELD_MEMBERS_NAMES(length, f1, f2, f3);
};

Array List Fields

Some communication protocols may define messages that transmit sequence of similar fields and/or raw data buffers. To make it easier to handle, the COMMS library provides comms::field::ArrayList field which provide a required interface to properly handle such sequences of data. It supports a sequence of raw bytes

using MySimpleList =
MyFieldBase,
std::uint8_t
>;
Field that represents a sequential collection of fields.
Definition: ArrayList.h:192

as well as using sequence of any fields defined in comms::field namespace

using MyComplexList =
MyFieldBase,
MyBundle<> // Complex bundle field, defined in previous section
>;

By default the read operation on comms::field::ArrayList continues as long as there is data left in input buffer, and write operation serialises all the data stored in internal vector. These default behaviours can be changed using options described below.

Prefixing with Size Information

Very often variable size sequences of raw bytes or other fields get prefixed with size information. The default behaviour of the comms::field::ArrayList is to read until the end of the buffer. Having sequence prefixed with number of elements to follow, allows earlier termination of the read operation, and allows having other independent fields to be appended after the sequence. The comms::field::ArrayList class supports comms::option::def::SequenceSizeFieldPrefix option that allows to specify type of the size field (usually a variant of comms::field::IntValue) to be serialised before the contents of comms::field::ArrayList being serialised. For example, the serialised raw bytes sequence is prefixed with 2 bytes of size information:

using MyList =
MyFieldBase,
std::uint8_t,
>;
static const std::uint8_t InputBuffer[] = {
0x0, 0x3, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf
}
static const auto InputBufferSize = std::extent<decltype(InputBuffer)>::value;
MyList myList;
const auto* readIter = &InputBuffer[0];
auto es = myList.read(readIter, InputBufferSize);
assert(es == comms::ErrorStatus::Success); // No error is expected;
assert(myList.value().size() == 3U); // Reading only 3 elements
assert((myList.value())[0] == 0xa); // First element
assert((myList.value())[1] == 0xb); // Second element
assert((myList.value())[2] == 0xc); // Third element
assert(std::distance(&InputBuffer[0], readIter) == 5); // Expected to consume 2 first bytes of the size + ,
// number of elements size specified (=3). Overall 5 bytes consumed
std::vector<std::uint8_t> outputBuffer;
auto writeIter = std::back_inserter(outputBuffer);
es = myList.write(writeIter, outputBuffer.max_size());
assert(es == comms::ErrorStatus::Success); // No error is expected;
assert(outputBuffer.size() == 5U); // Expected to write 5 bytes, 2 bytes for size, and 3 for elements.
assert(std::equal(outputBuffer.begin(), outputBuffer.end(), std::begin(InputBuffer)); // The output must be equal to
ErrorStatus read(TIter &iter, std::size_t len)
Read field value from input data sequence.
Definition: ArrayList.h:408
Option that modifies the default behaviour of collection fields to prepend the serialised data with n...
Definition: options.h:438

Some protocols prefix the sequence with serialisation length rather than number of elements to follow. In this case the comms::option::def::SequenceSerLengthFieldPrefix option needs to be used instead of comms::option::def::SequenceSizeFieldPrefix.

Element Serialisation Length Prefix

Also some protocols, for easier exchange of lists between nodes that use different versions of the same protocol, may require prefixing every element of the list with its serialisation length. In this case comms::option::def::SequenceElemSerLengthFieldPrefix option may be used. For example, the list of bundles prefixed with 2 bytes specifying number of elements to follow, and with every element prefixed with its serialisation length using variable length base-128 encoding may look like this:

using ElemLengthPrefixField =
MyFieldBase,
std::uint32_t,
comms::option::def::VarLength<1, 4> // variable length encoding up to 4 bytes
>;
using MyList =
MyFieldBase,
MyBundle, // some bundle of fields
>;
Option that forces every element of comms::field::ArrayList to be prefixed with its serialisation len...
Definition: options.h:475
Option used to specify that field may have variable serialisation length.
Definition: options.h:337

When every element of the list is of fixed size, i.e. has the same serialisation length, it becomes redundant to prefix every element with its length. Instead, only first element can be prefixed with one, and all others may reuse the same information. To achieve such behaviour comms::option::def::SequenceElemFixedSerLengthFieldPrefix should be used instead. For example, the list of fixed length bundles prefixed with 1 byte specifying number of elements to follow, and with first element prefixed with 1 byte containing its serialisation length may look like this:

Detached Size Information

There may be cases when size information is detached from the sequence itself, i.e. there are other fields between the size field and the sequence itself. For example, the protocol specifies the following:

Byte Offset Length Description
0 1 Number of elements in sequence
1 1 Some flags bitmask
2 2 * N Sequence of 2 byte integral values

In this case the option comms::option::def::SequenceSizeFieldPrefix can NOT be used. In fact the size information is not a part of the sequence any more, it must be a separate independent field. When this field is successfully read, its value must be forced upon the sequence somehow before the read operation of the sequence takes place. To help with such forcing, comms::option::def::SequenceSizeForcingEnabled option was introduced. When this option used, the comms::field::ArrayList::forceReadElemCount member function of the field may be used to force number of elements that follow.

using MyList =
MyFieldBase,
>;
static const std::uint8_t InputBuffer[] = {
0x3, 0xff, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf
}
static const auto InputBufferSize = std::extent<decltype(InputBuffer)>::value;
const auto* readIter = &InputBuffer[0];
auto remSize = InputBufferSize;
SeqSizeField sizeField;
auto es = sizeField.read(readIter, remSize);
assert(es == comms::ErrorStatus::Success); // No error is expected;
assert(sizeField.value() == 3U); // First byte should be read;
remSize -= sizeField.length();
BitmaskField bitmask;
es = bitmask.read(readIter, remSize);
assert(es == comms::ErrorStatus::Success); // No error is expected;
assert(bitmask.value() == 0xff); // Second byte should be read;
remSize -= bitmask.length();
MyList myList;
myList.forceReadElemCount(sizeField.value()); // Force number of elements to read
es = myList.read(readIter, remSize);
assert(es == comms::ErrorStatus::Success); // No error is expected;
assert(myList.value().size() == 3U); // Reading only 3 elements
assert((myList.value())[0] == 0xa); // First element
assert((myList.value())[1] == 0xb); // Second element
assert((myList.value())[2] == 0xc); // Third element
Option to enable external forcing of the collection's elements count.
Definition: options.h:544

Forcing Element Serialisation Length

In addition to prefixing variable length lists with amount of elements to follow, some protocols may also prefix them with serialisation length of the single element. Such technique is usually used to maintain data exchange compatibility with earlier versions of the protocol, which may be used on the other side of the communication link. The COMMS library allows forcing the serialisation length of a single element when such information becomes available. It is similar to Detached Size Information. The option comms::option::def::SequenceElemLengthForcingEnabled needs to be used when defining the field type, and comms::field::ArrayList::forceReadElemLength() and comms::field::ArrayList::clearReadElemLengthForcing() functions to set/clear the forcing information.

// Common base class for all the fields
// Field used to serialise serialisation length of a single element in the list
// Field used to serialise number of elements in the list
using MyList =
MyFieldBase,
comms::field::IntValue< // 3 bytes integers
MyFieldBas,
std::uint32_t,
comms::option::def::SequenceElemLengthForcingEnabled // enable forcing of the element length
>;
static const std::uint8_t InputBuffer[] = {
0x4, // single element serialisation length
0x2, // number of elements in the list,
0xa, 0xa, 0xa, 0x0, // first element + padding
0xb, 0xb, 0xb, 0x0 // second element + padding
}
static const auto InputBufferSize = std::extent<decltype(InputBuffer)>::value;
ElemLengthPrefixField lengthPrefix;
MyList myList;
const auto* readIter = &InputBuffer[0];
auto es = lengthPrefix(readIter, InputBufferSize);
assert(es == comms::ErrorStatus::Success); // No error is expected;
assert(lengthPrefix.value() == 4U);
myList.forceReadElemLength(lengthPrefix.value()); // force serialisation length of the single element
auto es = myList.read(readIter, InputBufferSize - lengthPrefix.length());
assert(es == comms::ErrorStatus::Success); // No error is expected;
assert(myList.value().size() == 2U); // Reading only 2 elements
assert((myList.value())[0] == 0x0a0a0a); // First element
assert((myList.value())[1] == 0x0b0b0b); // Second element
assert(std::distance(&InputBuffer[0], readIter) == 10); // Expected to consume in the
Option to enable external forcing of the collection element serialisation length.
Definition: options.h:570

Terminating Sequence with Suffix

Sometimes there is no information about size of the sequence up front. It may be terminating using some kind of special value. For example, the sequence of raw bytes is terminated by the value of 0. Such termination is achieved by using comms::option::def::SequenceTerminationFieldSuffix option.

using TermField = comms::field::IntValue<MyFieldBase, std::uint8_t>; // Default value is 0.
using MyList =
MyFieldBase,
>;
static const std::uint8_t InputBuffer[] = {
0x1, 0x2, 0x3, 0x4, 0x0, 0xa, 0xb, 0xc
}
static const auto InputBufferSize = std::extent<decltype(InputBuffer)>::value;
const auto* readIter = &InputBuffer[0];
MyList myList;
es = myList.read(readIter, InputBufferSize);
assert(es == comms::ErrorStatus::Success); // No error is expected;
assert(myList.value().size() == 4U); // Reading only 4 elements, terminating 0 is not included
assert((myList.value())[0] == 0x1); // First element
assert((myList.value())[1] == 0x2); // Second element
assert((myList.value())[2] == 0x3); // Third element
assert((myList.value())[4] == 0x4); // Fourth element
assert(std::distance(&InputBuffer[0], readIter) == 5); // Expected to consume all bytes including termination one
Option that forces termination of the sequence when predefined value is encountered.
Definition: options.h:507

Fixed Size Sequences

In many cases the size of the sequence is defined in the protocol without any prefix or suffix to define the length of the sequence. To define such sequence comms::option::def::SequenceFixedSize option should be used. Below is example of how to define sequence of four unsigned 16 bit integer values.

using MyList =
MyFieldBase,
>;
static const std::uint8_t InputBuffer[] = {
0x0, 0x1, 0x0, 0x2, 0x0, 0x3, 0x0, 0x4, 0xa, 0xb, 0xc
}
static const auto InputBufferSize = std::extent<decltype(InputBuffer)>::value;
const auto* readIter = &InputBuffer[0];
MyList myList;
es = myList.read(readIter, InputBufferSize);
assert(es == comms::ErrorStatus::Success); // No error is expected;
assert(myList.value().size() == 4U); // Reading only 4 elements
assert((myList.value())[0] == 0x1); // First element
assert((myList.value())[1] == 0x2); // Second element
assert((myList.value())[2] == 0x3); // Third element
assert((myList.value())[4] == 0x4); // Fourth element
assert(std::distance(&InputBuffer[0], readIter) == 8); // Consumed only 4 element (2 bytes each)
Option used to define exact number of elements in the collection field.
Definition: options.h:579

NOTE, that comms::option::def::SequenceFixedSize option insures existence of the right number of elements "on the wire", but doesn't influence number of elements in the newly created list field:

MyList myList;
auto& storageVector = myList.value();
assert(storageVector.empty());

Also nothing prevents from having too many values in the storage vector, but only specified number of the elements will be serialised:

myList.push_back(0x1); // will be serialised
myList.push_back(0x2); // will be serialised
myList.push_back(0x3); // will be serialised
myList.push_back(0x4); // will be serialised
myList.push_back(0x5); // WON'T be serialised

Value Storage

By default, the internal data is stored using std::vector.

MySimpleList simpleList; // defined above
auto& simpleListStorage = simpleList.value(); // reference to std::vector<std::uint8_t>;
MyComplexList complexList; // defined above
auto& complexListStorage = complexList.value(); // reference to std::vector<MyBundle>;

This behaviour can be modified using extra options such as comms::option::app::CustomStorageType, comms::option::app::FixedSizeStorage, comms::option::app::OrigDataView, or comms::option::app::SequenceFixedSizeUseFixedSizeStorage. HOWEVER, these options do not influence the way how list fields are being serialised, they influence the way how list value has been stored. As the result, they should NOT be used in protocol definition. Instead, provide a way to to the actual application to modify the default storage by passing extra options. For example:

template <typename... TExtraOptions>
using MyList = comms::field::ArrayList<..., TExtraOptions...>;

All the Common Options or Modifications for the Fields are also applicable to comms::field::ArrayList field.

String Fields

Many protocols have to transfer strings. They are defined using comms::field::String field.

Field that represents a string.
Definition: String.h:159

It is very similar to comms::field::ArrayList it terms of value storage, read/write operations, and supported options. By default the value is stored as std::string.

MyString myStr;
auto& myStrStorage = myStr.value(); // Reference to std::string.

Just like described in Value Storage section above the same options can be used to modify the storage type of the comms::field::String field, but should NOT be used in protocol definition, but instead there should be an ability to provide extra options.

template <typename... TExtraOptions>
using MyString = comms::field::String<MyFieldBase, TExtraOptions...>;

Prefixing string with single byte of the size information will look like this:

using MyString =
MyFieldBase,
>;
MyString myStr;
myStr.value() = "hello";
std::vector<std::uint8_t> outputBuf;
auto writeIter = std::back_inserter(outputBuf);
auto es = myStr.write(writeIter, outputBuf.max_size());
assert(es = comms::ErrorStatus::Success); // No error is expected
assert(outputBuf.size() == 6U); // 1 byte of size, followed by 5 characters of "hello" string
assert(outputBuf[0] == 5U); // size info
assert(outputBuf[1] == 'h');
assert(outputBuf[2] == 'e');
assert(outputBuf[3] == 'l');
assert(outputBuf[4] == 'l');
assert(outputBuf[5] == 'o');

See also Prefixing with Size Information.

Encoding of zero termination strings without size prefix can be defined like this:

using ZeroTermField = comms::field::IntValue<MyFieldBase, std::uint8_t>; // default value is 0
using MyString =
MyFieldBase,
>;
MyString myStr;
myStr.value() = "hello";
std::vector<std::uint8_t> outputBuf;
auto writeIter = std::back_inserter(outputBuf);
auto es = myStr.write(writeIter, outputBuf.max_size());
assert(es = comms::ErrorStatus::Success); // No error is expected
assert(outputBuf.size() == 6U); // 5 characters of "hello" string followed by zero termination suffix
assert(outputBuf[0] == 'h');
assert(outputBuf[1] == 'e');
assert(outputBuf[2] == 'l');
assert(outputBuf[3] == 'l');
assert(outputBuf[4] == 'o');
assert(outputBuf[5] == 0U);

See also Terminating Sequence with Suffix.

Another string example is to have zero terminated string, the serialisation of which occupies exactly 32 bytes, i.e. the string may have up to 31 non-zero characters. If string is too short, the serialisation data is padded by zeros until full length of 32 characters is produced.

using ZeroTermField = comms::field::IntValue<MyFieldBase, std::uint8_t>; // default value is 0
using MyString =
MyFieldBase,
>;
MyString myStr;
myStr.value() = "hello";
std::vector<std::uint8_t> outputBuf;
auto writeIter = std::back_inserter(outputBuf);
auto es = myStr.write(writeIter, outputBuf.max_size());
assert(es = comms::ErrorStatus::Success); // No error is expected
assert(outputBuf.size() == 32); // 5 characters of "hello" string followed by zero padding
assert(outputBuf[0] == 'h');
assert(outputBuf[1] == 'e');
assert(outputBuf[2] == 'l');
assert(outputBuf[3] == 'l');
assert(outputBuf[4] == 'o');
assert(outputBuf[5] == 0U);
...
assert(outputBuf[31] == 0U);
Option that forces collection fields to append provides suffix every time it is serialised.
Definition: options.h:531

NOTE, that the example above uses comms::option::def::SequenceTrailingFieldSuffix option, rather than comms::option::def::SequenceTerminationFieldSuffix. The options slightly differ. The "termination" one (comms::option::def::SequenceTerminationFieldSuffix) forces the field to stop reading when termination value is encountered, while "trailing" one (comms::option::def::SequenceTrailingFieldSuffix) doesn't check what it reads, the reading size must be limited by other means (comms::option::def::SequenceFixedSize in the example above). When the read is complete, it just consumes the termination character. Both options, however, force the termination character to be appended at the end during write operation.
Also note, that size limit is specified (using comms::option::def::SequenceFixedSize) to be 31. One more byte is added by the "trailing" suffix to complete to 32 bytes.

Just like with comms::field::ArrayList, it is possible to use static storage for fixed size strings:

using MyString =
...,
>;
Option that forces usage of fixed size storage for sequences with fixed size.
Definition: options.h:1372

or

using MyString =
...,
>;
Option that forces usage of embedded uninitialised data area instead of dynamic memory allocation.
Definition: options.h:1346

HOWEVER, the app options should be used in protocol definition, only in application customization.

Floating Point Value Fields

Floating point value fields (comms::field::FloatValue) are very similar to Integral Value Fields, but use float or double as its internal storage type. They abstract the IEEE 754 floating point values, which are serialised "as is" with either big or little endian encoding. The floating point value fields also support Value Units conversions.

Optional Fields

Some protocols may define optional fields, which may exist or be missing based on information recorded in other fields. For example there is a "flags" bitmask field which specifies whether the following field exists or missing. The optional field may also be tentative, i.e. if there is enough data in the input buffer it exists, and missing otherwise. The COMMS library provides comms::field::Optional which is a mere wrapper around other fields and provides an ability to set the optional state of the field. Let's do the example of the int32 field existence based on bit 0 in processing bitmask:

using OptField =
>;
FlagsField flags;
OptField optField;
// Common read function for multiple buffers
auto readFunc =
[&flags, &optField](const std::uint8_t*& iter, std::size_t len)
{
auto es = flags.read(iter, len);
assert(es == comms::ErrorStatus::Success); // No error is expected;
optField.setMissing();
if ((flags.value() & 0x1) != 0) {
optField.setExists();
}
es = optField.read(iter, len - flags.length());
assert(es == comms::ErrorStatus::Success); // No error is expected;
};
static const std::uint8_t InputBuffer1[] = {
0x1, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf
}
static const auto InputBuffer1Size = std::extent<decltype(InputBuffer1)>::value;
auto* readIter = &InputBuffer1[0];
readFunc(readIter, InputBuffer1Size);
assert(std::distance(&InputBuffer1[0], readIter) == 5); // Expected to read 1 byte of flags and
// 4 bytes of int32_t int value, because
// bit 0 in flags is set.
assert(optField.field().value() == 0x0a0b0c0d); // value is expected to be updated;
static const std::uint8_t InputBuffer2[] = {
0x0, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf
}
static const auto InputBuffer2Size = std::extent<decltype(InputBuffer2)>::value;
optField.field().value() = 0;
readIter = &InputBuffer2[0];
readFunc(readIter, InputBuffer2Size);
assert(std::distance(&InputBuffer2[0], readIter) == 1); // Expected to read only 1 byte of flags
// skipping read of int32_t int value, because
// bit 0 in flags is cleared.
assert(optField.field().value() == 0); // value is expected NOT to be updated;
ValueType & value()
Get access to the value storage.
Definition: ArrayList.h:365
Adaptor class to any other field, that makes the field optional.
Definition: Optional.h:45

Note, that default mode for the optional field is "tentative", which is updated after read operation:

OptField optField1;
assert(optField1.getMode() == comms::field::OptionalMode::Tentative); // Default mode is tentative
static const std::uint8_t InputBuffer[] = {
0x11, 0x22, 0x33, 0x44
}
static const auto InputBufferSize = std::extent<decltype(InputBuffer)>::value;
auto* readIter = &InputBuffer[0];
auto es = optField1.read(readIter, InputBufferSize);
assert(std::distance(&InputBuffer[0], readIter) == 4); // Expected to read 4 bytes of int32_t int value
assert(optField1.getMode() == comms::field::OptionalMode::Exists); // Mode is changed
OptField optField2;
assert(optField2.getMode() == comms::field::OptionalMode::Tentative); // Default mode is tentative
readIter = &InputBuffer[0];
es = optField2.read(readIter, 0); // Note 0 as a buffer size
assert(std::distance(&InputBuffer[0], readIter) == 0); // Expected not to read anything
assert(optField2.getMode() == comms::field::OptionalMode::Missing); // Mode is changed
@ Exists
Field must exist.
@ Missing
Field doesn't exist.

It is easy to change the default mode of the comms::field::Optional field by providing comms::option::def::DefaultOptionalMode option with selected default mode or comms::option::def::MissingByDefault / comms::option::def::ExistsByDefault aliases.

using OptField =
comms::option::def::MissingByDefault // Set default mode to be "missing"
>;
Option that specifies default initialisation class.
Definition: options.h:616

Some protocols may include version information either in transport framing or in one of the messages. Such info may specify whether a specific field exists or not. Such fields need to be wrapped in comms::field::Optional field, which receives comms::option::def::ExistsBetweenVersions option to specify the numeric versions of the protocol between which the field exists.

using OptField =
comms::option::def::MissingByDefault, // Set default mode to be "missing"
>;
DefaultOptionalMode< comms::field::OptionalMode::Missing > MissingByDefault
Alias to DefaultOptionalMode.
Definition: options.h:1056
Mark an comms::field::Optional field as existing between specified versions.
Definition: options.h:1177

If the field has been introduced in one of the version, but hasn't been removed yet, it is possible to use comms::option::def::ExistsSinceVersion alias to comms::option::def::ExistsBetweenVersions. Or the opposite, if the field has been introduced in the first version, but deprecated and removed in the later one, use comms::option::def::ExistsUntilVersion alias.

Usage of such version control option will automatically mark the optional field as existing or missing based on the provided version info in the setVersion() member function.

Variant Fields

Some protocols may require usage of heterogeneous fields or lists of heterogeneous fields, i.e. the ones that can be of multiple types. Good example would be a list of properties, where every property is a key/value pair or a TLV (type/length/value) triplet. The key (or type) is usually a numeric ID of the property, while value can be any field of any length. As an example for the key/value pairs let's define three value types:

  • Unsigned integer with length of only 1 byte (Value1)
  • Unsigned integer with length of 4 bytes (Value2)
  • String field with 1 byte size prefix (Value3)

The COMMS library provides comms::field::Variant field to allow such heterogeneous fields. Let's implement the described example.

The common key type is easy to represent as enum.

enum class KeyId : std::uint8_t
{
Key1,
Key2,
Key3,
};
@ NumOfValues
Number of available values, must be last.

And the relevant key fields as a variant of comms::field::EnumValue with only single acceptable value.

template <KeyId TId>
using KeyField =
MyFieldBase,
KeyId,
>;
using Key1 = KeyField<KeyId::Key1>;
using Key2 = KeyField<KeyId::Key2>;
using Key3 = KeyField<KeyId::Key3>;
Option that forces field's read operation to fail if invalid value is received.
Definition: options.h:674
Provide range of valid numeric values.
Definition: options.h:956

Please pay attention to usage of comms::option::def::FailOnInvalid option and having only single valid value in order to force failure of the read operation when the key doesn't match.

Then the KeyX and its corresponding ValueX need to be bundled together as a single PropertyX field.

template <typename TKey, typename TValue>
class Property : public
MyFieldBase,
std::tuple<
TKey,
TValue
>
>
{
using Base = ...; // repeat base definition if needed
public:
};
using Property1 = Property<Key1, Value1>;
using Property2 = Property<Key2, Value2>;
using Property3 = Property<Key3, Value3>;

Now we need a single field abstraction, which can be any of the specified above forms. The comms::field::Variant field class provides such an ability. As its second parameter it receives a tuple of supported types.

class MyVariant : public
MyFieldBase,
std::tuple<Property1, Property2, Property3>
>
{
// (Re)definition of the base class as inner Base type.
using Base = comms::field::Variant<...>;
public:
COMMS_VARIANT_MEMBERS_NAMES(prop1, prop2, prop3);
};
Defines a "variant" field, that can contain any of the provided ones.
Definition: Variant.h:79
#define COMMS_VARIANT_MEMBERS_NAMES(...)
Provide names for member fields of comms::field::Variant field.
Definition: Variant.h:931

Similar to COMMS_FIELD_MEMBERS_NAMES() macro for Bundle Fields, the COMMS_VARIANT_MEMBERS_NAMES() macro generates the following convenience member enum and functions

struct MyVariant : public comms::field::Variant<...>
{
// Enumerator to access fields
enum FieldIdx {
FieldIdx_prop1,
FieldIdx_prop2,
FieldIdx_prop3,
FieldIdx_numOfValues
}
// Initialize internal storage as "prop1"
template <typename... TArgs>
auto initField_prop1(TArgs&&... args) -> decltype(initField<FieldIdx_prop1>(std::forward<TArgs>(args)...))
{
return initField<FieldIdx_prop1>(std::forward<TArgs>(args)...)
}
// Access internal storage already initialized as "prop1"
auto accessField_prop1() -> decltype(accessField<FieldIdx_prop1>())
{
return accessField<FieldIdx_prop1>();
}
// Access internal storage already initialized as "prop1" (const variant)
auto accessField_prop1() const -> decltype(accessField<FieldIdx_prop1>())
{
return accessField<FieldIdx_prop1>();
}
// Initialize internal storage as "prop2"
template <typename... TArgs>
auto initField_prop2(TArgs&&... args) -> decltype(initField<FieldIdx_prop2>(std::forward<TArgs>(args)...))
{
return initField<FieldIdx_prop2>(std::forward<TArgs>(args)...)
}
// Access internal storage already initialized as "prop2"
auto accessField_prop2() -> decltype(accessField<FieldIdx_prop2>())
{
return accessField<FieldIdx_prop2>();
}
// Access internal storage already initialized as "prop2" (const variant)
auto accessField_prop2() const -> decltype(accessField<FieldIdx_prop2>())
{
return accessField<FieldIdx_prop2>();
}
// Initialize internal storage as "prop3"
template <typename... TArgs>
auto initField_prop3(TArgs&&... args) -> decltype(initField<FieldIdx_prop3>(std::forward<TArgs>(args)...))
{
return initField<FieldIdx_prop3>(std::forward<TArgs>(args)...)
}
// Access internal storage already initialized as "prop3"
auto accessField_prop3() -> decltype(accessField<FieldIdx_prop3>())
{
return accessField<FieldIdx_prop3>();
}
// Access internal storage already initialized as "prop3" (const variant)
auto accessField_prop3() const -> decltype(accessField<FieldIdx_prop3>())
{
return accessField<FieldIdx_prop3>();
}
// Redefinition of the members fields types:
using Field_prop1 = ...;
using Field_prop2 = ...;
using Field_prop3 = ...;
};

NOTE, that the provided names have propagated into definition of FieldIdx enum, all initField_* and accessField_* functions, as well as inner Field_* types definitions.

Now it is easy to put such Variant field type into the list:

In this scenario, read operation on the list will invoke read operation of every MyVariant field, which in turn will try to perform read operation on Property1, Property2, and Property3 in the order of their definition inside the provided tuple. The read operation of the comms::field::Variant field type will stop when read operation of any of the contained types reports comms::ErrorStatus::Success as its status. That's why every key field needs to fail its read operation on invalid value.

The extension of the key/value pairs example above to TLV triplets is quite easy.

// No need for length prefix for strings any more
...
template <typename TKey, typename TValue>
class Property : public
MyFieldBase,
std::tuple<
TKey,
comms::field::IntValue<MyFieldBase, std::uint16_t>, // 2 byte value of remaining length
TValue
>,
comms::option::def::RemLengthMemberField<1> // Index of remaining length field is 1
>
{
using Base = ...; // repeat base definition
public:
COMMS_FIELD_MEMBERS_NAMES(key, length, value);
};

NOTE usage of comms::option::def::RemLengthMemberField option described earlier in Bundle Fields section. It informs the COMMS library about presence of the length value in the bundle, which will result in proper read length limitation for the value(s) to follow. Also in case the read of the following value(s) does not consume all the reported length, COMMS library will advance the read iterator to consume the remaining bytes resulting in read of the next field to be at correct location.

The length field in example above contains remaining length not including the field itself. Some protocols may include the length of the length field itself. If this is the case, just use comms::option::def::NumValueSerOffset option (described in Serialisation Offset) to add extra numeric offset to be added when the field's value is written.

template <typename TKey, typename TValue>
class Property : public
MyFieldBase,
std::tuple<
TKey,
comms::field::IntValue<
MyFieldBase,
std::uint16_t,
comms::option::def::NumValueSerOffset<sizeof(std::uint16_t)>
>, // 2 byte value of remaining length
TValue
>,
comms::option::def::RemLengthMemberField<1> // Index of remaining length field is 1
>
{
using Base = ...; // repeat base definition
public:
COMMS_FIELD_MEMBERS_NAMES(key, length, value);
};

RECOMMENDATION: In case of TLV property triplets it is recommended to create a dummy field with non-failing read to allow usage of unknown properties which can be introduced in later versions of the protocol.

class UnknownProperty : public
MyFieldBase,
std::tuple<
comms::field::IntValue<MyFieldBase, std::uint8_t>, // storage of unknown key
comms::field::IntValue<MyFieldBase, std::uint16_t>, // 2 byte value of remaining length
comms::field::ArrayList<MyFieldBase, std::uint8_t>, // storage or raw data
>,
comms::option::def::RemLengthMemberField<1> // Index of remaining length field is 1
>
{
using Base = ...; // repeat base definition
public:
COMMS_FIELD_MEMBERS_NAMES(key, length, value);
};

And put such property at the end of the supported types tuple for the variant field definition.

struct MyVariant : public
MyFieldBase,
std::tuple<Property1, Property2, Property3, UnknownProperty>
>
{
...
};

The default constructed comms::field::Variant object from the examples above has an "invalid" state, i.e. hasn't been initialized and doesn't contain any valid field. It can be changed by providing comms::option::def::DefaultVariantIndex option.

struct MyVariant : public
MyFieldBase,
std::tuple<Property1, Property2, Property3>,
comms::option::def::DefaultVariantIndex<0> // Initialise as Prop1
>
{
COMMS_VARIANT_MEMBERS_NAMES(prop1, prop2, prop3);
};

When instantiating such MyVariant object, there is no need to perform initialization (construction) of the contained object.

MyVariant var;
assert(var.currentFieldValid());
assert(var.currentField() == 0U); // Make sure the current index is 0
auto& prop1 = var.accessField_prop1(); // Get access to Property1 interface
...

SIDE NOTE: In addition to COMMS_VARIANT_MEMBERS_NAMES() macro there is COMMS_VARIANT_MEMBERS_ACCESS() one. It is very similar to COMMS_VARIANT_MEMBERS_NAMES() but does NOT (re)define the inner Field_* types. It also does not require (except for clang) having base class to be (re)defined as inner Base type.

class MyVariant : public comms::field::Variant<...>
{
public:
COMMS_VARIANT_MEMBERS_ACCESS(prop1, prop2, prop3);
}

In fact COMMS_VARIANT_MEMBERS_NAMES() is implemented as the wrapper around COMMS_VARIANT_MEMBERS_ACCESS().

Common Options or Modifications for the Fields

There are options that suitable only to numeric fields, such as comms::field::IntValue, comms::field::EnumValue, comms::field::BitmaskValue.
There are options that suitable only for collection fields, such as comms::field::ArrayList, and comms::field::String.
There are also common options that can be used with all the fields that support options.

Default Value for Default Construction

There may be a case when default construction of the field object should assign some custom value to the field, which differ to the usual defaults, such as assigning 0 to numeric fields or empty string to a string field.

One of the possible ways is to extend the defined field class and set the required value in the constructor.

struct MyString : public comms::field::String<MyFieldBase>
{
MyString()
{
value() = "hello";
}
};
ValueType & value()
Get access to the value storage.
Definition: String.h:366

Another way is to use comms::option::def::DefaultValueInitialiser option. It receives a template parameter, which has to be a type of initialisation class. It must provide operator() which is responsible to assign a custom value to the field. It is going to be invoked from the default constructor of the field.

struct Initaliser
{
template <typename TField>
void operator()(TField& field)
{
field.value() = ...; // Set the default value here
}
};

For example:

struct CustomStringInitaliser
{
template <typename TField>
void operator()(TField& field)
{
field.value() = "hello"
}
};
using MyString =
MyFieldBase,
>;
MyString myStr; // Default construction
assert(myStr.value() == "hello"); // Custom default value is expected to be assigned

NOTE that the used operator() specifies the field type as a template parameter. This is required because the passed reference is to one of the defined field's base classes, which is implementation dependent. Just use the provided value() member function to access the value.

The COMMS library also provides a simpler alias for comms::option::def::DefaultValueInitialiser to set default value for numeric fields. It is comms::option::def::DefaultNumValue.

using MyInt =
MyFieldBase,
std::uint16_t,
>;
MyInt myInt;
assert(myInt.value() == 10); // Custom default value is expected to be assigned

Custom Read Functionality

Every field provides read() member function to perform read of the field's value. Sometimes the default "read" functionality may be incorrect or incomplete. For example, let's define a "bundle" field with two members. The first one is a "bitmask", while the second one is optional 2 byte "int" value. The second member exists only if least significant bit of the "bitmask" is not 0. In this case, the provided read() member function won't analyse the value of the read "bitmask" and won't modify "existing"/"missing" mode value of the optional field.

One way to implement custom read functionality is to extend the field definition and override the read() member function:

class MyBundle : public
MyFieldBase,
std::tuple<
comms::field::BitmaskValue<MyFieldBase, comms::option::def::FixedLength<1> >,
comms::field::Optional<MyFieldBase, std::uint16_t>
>,
comms::option::def::HasCustomRead
>
{
using Base = comms::field::Bundle<...>;
public:
// Provide convenience access functions
template <typename TIter>
comms::ErrorStatus read(TIter& iter, std::size_t len)
{
using Base = typename std::decay<decltype(comms::field::toFieldBase(*this))>::type;
auto es = Base::template readUntilAndUpdateLen<FieldIdx_value>(iter, len);
return es;
}
if (field_mask().getBitValue(0)) {
field_value().setExists();
}
else {
field_value().optInt.setMissing();
}
return Base::template readFrom<FieldIdx_value>(iter, len)
}
};
ErrorStatus read(TIter &iter, std::size_t size)
Read field value from input data sequence.
Definition: Bundle.h:306
Bundle< TFieldBase, TMembers, TOptions... > & toFieldBase(Bundle< TFieldBase, TMembers, TOptions... > &field)
Upcast type of the field definition to its parent comms::field::Bundle type in order to have access t...
Definition: Bundle.h:805

Please NOTE the following:

Custom Value Validation Logic

Every field provides valid() member function to validate the internal value. By default, every internal value of the field is considered to be valid, i.e. the valid() function will always return true.

One of the ways to provide custom validation logic is to extend the field definition and implement valid() member function:

struct MyString : public comms::field::String<MyFieldBase>
{
bool valid() const
{
// Valid if not empty and starts with '$'
return (!value().empty()) && (value()[0] == '$');
}
};

Quite often the valid values of the numeric fields can be expressed in limited number of ranges: [minValid - maxValid]. The COMMS library provides comms::option::def::ValidNumValueRange option (and comms::option::def::ValidNumValue alias), which can be used multiple times. The field's value is considered to be valid if at least one of the provided ranges contains it. The range validation option can be used only with numeric value fields, such as comms::field::IntValue, or comms::field::EnumValue. For example:

enum SomeEnum : std::uint8_t
{
SomeEnum_Value1 = 1,
SomeEnum_Value2,
SomeEnum_Value3
};
using MyEnum =
MyFieldBase,
SomeEnum,
>;
MyEnum myEnum;
assert(myEnum.value() == SomeEnum_Value1);
assert(myEnum.valid());
myEnum.value() = static_cast<SomeEnum>(0); // Assigning invalid value.
assert(!myEnum.valid()); // The field being invalid must be reported

Another example could be a single character numeric field with valid values range of [0x20 - 0x7e], as well as value 0. Such field can be defined as:

WARNING: Some older compilers (gcc-4.7) fail to compile valid C++11 code that allows usage of multiple comms::option::def::ValidNumValueRange options. If this is the case, please don't pass more than one comms::option::def::ValidNumValueRange option.

There is a also a convenience reserved bits checked option for use with bitmasks (comms::field::BitmaskValue). Many bitmask fields may have one or several reserved bits with predefined values they must contain. The option is comms::option::def::BitmaskReservedBits. It receives two template parameters: one for the mask indicating the reserved bits and another for the expected values of these bits.
For example, below is a definition of the 1 byte bitmask field that has two reserved bits, most and least significant. Both of them must be 0.

using MyFlags =
MyFieldBase,
>;
MyFlags flags;
assert(myEnum.valid());
flags.value() |= 0x1; // set bit 0;
assert(!flags.valid()); // the field is invalid now.

Custom Refresh Functionality

Every field provides refresh() member function used to bring the field's value into a consistent state. By default this function does nothing and returns false, meaning the field has NOT been updated. For complex fields, such as comms::field::Bitfield or comms::field::Bundle, the default behaviour is to invoke refresh() member function of each member field and return true if any of the calls returned true.

One way to change such default behaviour is to extend the field definition and implement refresh() member function. Let's take the same example as in Custom Read Functionality section. There is a "bundle" field with two members. The first one is a "bitmask", while the second one is optional 2 byte "int" value. The second member exists only if least significant bit of the "bitmask" is not 0. There is a chance of having inconsistent state when the least significant bit in the "bitmask" is set, but the optional "int" field is marked to be "missing". There is a need to provide the custom "refresh" logic that brings the field's contents into a consistent state.

struct MyBundle : public
MyFieldBase,
std::tuple<
comms::field::BitmaskValue<MyFieldBase, comms::option::def::FixedLength<1> >,
comms::field::Optional<MyFieldBase, std::uint16_t>
>,
comms::option::def::HasCustomRead,
comms::option::def::HasCustomRefresh
>
{
...
bool refresh()
{
if (field_mask().getBitValue(0)) {
}
if (field_value().getMode() == expectedMode) {
return false; // Nothing has been changed
}
field_value().setMode(expectedMode);
return true; // Field has been updated
}
};
bool refresh()
Refresh the field's contents.
Definition: Bundle.h:625

NOTE the usage of comms::option::def::HasCustomRefresh option. It notifies other classes about existence of custom refresh functionality (instead of default one). Other classes may contain some inner logic to perform various optimisations if there is no custom refresh. Failure to specify this option may result in incorrect behaviour.

Custom Write Functionality

On some very rare occasions there may be a need to write custom write functionality. It can be achieved by writing custom write() member function

struct MyBundle : public
MyFieldBase,
std::tuple<...>,
comms::option::def::HasCustomWrite
>
{
template <typename TIter>
comms::ErrorStatus write(TIter& iter, std::size_t len) const
{
...
}
};
ErrorStatus write(TIter &iter, std::size_t size) const
Write current field value to output data sequence.
Definition: Bundle.h:495

NOTE usage of comms::option::def::HasCustomWrite option. It is required to let prevent field holding comms::MessageBase class from attempting to optimize write operation due to possible incorrect behavior.

Custom Version Update Functionality

Some protocols may include version information either in the transport framing of every message or in one of the messages used to establish connection. Such info may influence whether some fields exist, as well as modify other aspects of the fields, such as validity ranges.

Every field provides setVersion() member function used to notify it about the version change and isVersionDependent() one to inquire at compile time whether the field contents may change after such notification. For most fields setVersion() function does nothing and returns false, meaning the field has NOT been updated (similar to Custom Refresh Functionality). For complex fields, such as comms::field::Bitfield or comms::field::Bundle, the default behaviour is to invoke setVersion() member function of each member field and return true if any of the calls returned true. For comms::field::ArrayList fields, the version information may be stored inside (only if the element's isVersionDependent() member function returns true) and used to notify every new field that is being read during read operation.

Usually, the default version handling provided by the COMMS library is good enough. However, there may be cases when custom operation needs to be performed during version update. For example, there is integral value field with valid values range [0, 5]. There is a need to report field as being invalid for any other numbers for all version up to 5. However, since version 6 the range is extended to [0, 10]. It can be defined as following:

class MyInt : public
MyFieldBase,
std::uint8_t,
comms::option::def::ValidNumValueRange<0, 5>,
comms::option::def::HasCustomVersionUpdate
{
using Base = comms::field::IntValue<...>; // Repeat base class definition
public:
// Updated validity check
bool valid() const
{
if (Base::valid()) {
return true;
}
if (m_version < 6) {
return false;
}
return value() <= 10;
}
// Updated version set
// Store version internally for future references
bool setVersion(VersionType version)
{
m_version = version;
return Base::setVersion(version);
}
private:
VersionType m_version = 0;
};

NOTE, the usage of comms::option::def::HasCustomVersionUpdate option. It marks the defined field as "version dependent" and as the result its isVersionDependent() member function will return true.

Also NOTE, that by default the VersionType inner type is defined to be unsigned. If there is a need to change that, the comms::option::def::VersionType needs to be passed to the definition of the common base class of all the fields:

using MyFieldBase =
>;
Endian< comms::traits::endian::Big > BigEndian
Alias option to Endian specifying Big endian.
Definition: options.h:177
Provide type to be used for versioning.
Definition: options.h:1159

Fail on Invalid Value

Sometimes the protocol specifications may impose a strict rules on disallowing invalid values, such as the message must be dropped when some field has an invalid value. It is easy to implement by forcing read() operation on such field to fail when reading an invalid value is recognised. The COMMS library provides comms::option::def::FailOnInvalid option to help with such task. For example:

using MyField =
MyFieldBase,
std::uint8_t,
>;
static const std::uint8_t InvalidBuf[] = { 0x6 };
static const auto InvalidBufSize = std::extent<decltype(InvalidBuf)>::value;
MyField myField;
auto* readIter = &InvalidBuf[0];
auto es = myField.read(readIter, InvalidBufSize);
assert(es != comms::ErrorStatus::Success); // Read failure is expected
static const std::uint8_t ValidBuf[] = { 0x1 };
static const auto ValidBufSize = std::extent<decltype(ValidBuf)>::value;
readIter = &ValidBuf[0];
es = myField.read(readIter, ValidBufSize);
assert(es == comms::ErrorStatus::Success); // Read operation is expected to be successful now

Ignore Invalid Value

The COMMS library also provides comms::option::def::IgnoreInvalid option. It DOESN'T report failure on read operation when the invalid value is discovered (like comms::option::def::FailOnInvalid does). Instead the field's internal value remains unchanged, although the read iterator is advanced as if the value is read. For example:

using MyField = comms::field::IntValue<
MyFieldBase,
std::uint8_t,
>;
static const std::uint8_t InvalidBuf[] = { 0x6 };
static const auto InvalidBufSize = std::extent<decltype(InvalidBuf)>::value;
MyField myField;
assert(myField.valid());
assert(myField.value() == 0U);
auto* readIter = &InvalidBuf[0];
auto es = myField.read(readIter, InvalidBufSize);
assert(es == comms::ErrorStatus::Success); // No failure is expected
assert(myField.value() == 0U); // Value mustn't be changed
static const std::uint8_t ValidBuf[] = { 0x1 };
static const auto ValidBufSize = std::extent<decltype(ValidBuf)>::value;
readIter = &ValidBuf[0];
es = myField.read(readIter, ValidBufSize);
assert(es == comms::ErrorStatus::Success); // No failure is expected
assert(myField.value() == 1U); // Value is expected to be updated
Option that forces field's read operation to ignore read data if invalid value is received.
Definition: options.h:683

Empty Serialisation

Some protocols may define some constants which are NOT being sent over I/O link. Sometimes it may be useful to still treat these values as message fields. Usage of comms::option::def::EmptySerialization (or comms::option::def::EmptySerialisation for those who prefer British spelling) can be used to achieve such effect.

using MyField = comms::field::IntValue<
MyFieldBase,
std::uint8_t,
>;
MyField field;
assert(field.length() == 0U); // Not expected to have any serialisation length
assert(field.value() == 5U); // The value is still accessible;
std::vector<std::uint8_t> outBuf;
auto writeIter = std::back_inserter(outBuf);
auto es == field.write(writeIter, outBuf.max_size());
assert(es == comms::ErrorStatus::Success); // No failure is expected
assert(outBuf.empty()); // No data has been written
Force field not to be serialized during read/write operations.
Definition: options.h:1092

Allowing Further Use of Options

As was mentioned earlier there are options that define how fields are (de)serialised. These options are expected to be used in protocol fields definition. However, there are options that are application specific. They may change the data structures being used for storage, or modify default value and/or valid values. In order to allow such application specific modifications, the defined fields should allow further extension, for example with extra variadic template arguments.

template <typename... TExtraOpts>
using MyField =
comms::Field<comms::option::def::BigEndian>, // base class of the field
std::uint16_t,
TExtraOpts...,

Please note, that current implementation gives preference to the options defined earlier. That's why TExtraOpts... should be listed before any other options. It will allow setting other default value needed by the application, and/or override the valid values ranges (using comms::option::def::ValidRangesClear). For example:

using MyUpdatedField =
MyField<
>
Clear accumulated ranges of valid values.
Definition: options.h:961

Custom Fields

There may be a case when communication protocol demands implementation of some intricate field's logic that is not covered by the COMMS library. It is possible to provide custom implementation of the custom field and use it with other components provided by the library as long as it defines the following minimal interface:

class MyField
{
public:
// Length required to serialise current value
std::size_t length() const;
// Minimal length required to serialise any value this field may contain
static constexpr std::size_t minLength();
// Maximal length required to serialise any value this field may contain.
static constexpr std::size_t maxLength();
// Check validity of the internal value
bool valid() const;
// Bring field's value into the consistent state,
// return true if the field's value has been updated, false otherwise
bool refresh();
// Read field value from input data sequence, using any type of input iterator
template <typename TIter>
comms::ErrorStatus read(TIter& iter, std::size_t size);
// Write field value to output data sequence, using any type of output iterator
template <typename TIter>
comms::ErrorStatus write(TIter& iter, std::size_t size) const;
};
static constexpr std::size_t maxLength()
Get maximal length that is required to serialise field of this type.
Definition: IntValue.h:281
static constexpr std::size_t minLength()
Get minimal length that is required to serialise field of this type.
Definition: IntValue.h:274

The comms::Field class provides readData() and writeData() protected member functions that serialise data using endian provided as an option to the class. It makes sense to inherit from comms::Field with right option and reuse these functions inside:

class MyField : public comms::Field<comms::option::def::BigEndian>
{
public:
...
template <typename TIter>
comms::ErrorStatus read(TIter& iter, std::size_t size)
{
...
auto val = readData<InternalType>(iter);
...
}
template <typename TIter>
comms::ErrorStatus write(TIter& iter, std::size_t size) const
{
...
writeData(..., iter);
...
}
};
static void writeData(T value, TIter &iter)
Write data into the output buffer.
Definition: Field.h:128

Also to be consistent with the existing implementation of the fields in the COMMS library it is recommended to provide an accessor functions value() for internal data storage:

class MyField : public comms::Field<comms::option::def::BigEndian>
{
public:
using ValueType = ...;
ValueType& value() {...}
const ValueType& value() const {...}
};

Other Fields

With time the COMMS library may grow by adding support for some other built-in fields as well as supporting extra options to the existing fields described in this tutorial. If such new field and/or option is not described in this tutorial, it should be easy enough for the developer to master. Please refer to the documentation of the field and/or option itself.