COMMS
Template library intended to help with implementation of communication protocols.
Loading...
Searching...
No Matches
How to Use Defined Custom Protocol

This page is oriented to client application developers. It provides instructions on how to use protocol defininition after it was developed following How to Define New Custom Protocol instructions. All the examples below will use my_protocol namespace for classes that are expectected to be already defined by the custom protocol implementation.

Headers and Libraries

The protocol definition as well as COMMS library are headers-only. The protocol definition should already include all the required headers from the COMMS library. The client application will have to include only relevant headers from the protocol definition.

Paths and Namespaces

All the classes provided by the COMMS library reside in comms namespace and the inner include paths start with "comms/". There may be the case when this library being integrated into existing project that may already define and use its own module (namespace) named comms. In this case it should be possible to rename installation directory (comms to comms2) and to run a script after library installation to replace the following strings with new name:

  • "namespace comms" - replace with "namespace comms2"
  • "comms::" - replace with "comms2::"
  • "comms/" - replace with "comms2/"

Error Handling

The COMMS library is intended to be used in embedded systems (including bare metal), which means the library does not use exceptions to report errors. The runtime errors are reported via comms::ErrorStatus return values. All pre- and post-conditions are checked using COMMS_ASSERT() macro. It is, just like regular standard assert(), is compiled in if NDEBUG symbol is not defined. In case the provided condition doesn't hold true, the macro checks whether custom assertion failure behaviour was registered. If yes, the registered custom assertion failure report is invoked, otherwise the standard failure report used by standard assert() macro is used. If COMMS library is used in bare metal environment without standard library, the COMMS_NOSTDLIB symbol should be defined. In this case infinite loop is a default assertion failure report behaviour.

See Custom Assertion Failure Behaviour for details on how to define custom assertion failure behaviour.

Configuration Options

The Protocol Definition classes are expected to define all the relevant protocol classes in a way that provides an ability to perform end application specific customization, such as choice of specific data types and/or necessary polymorphic interfaces. Such customization is performed by passing various options defined in comms::option::app namespace to the relevant class definitions. The comms::option::def namespace contains all the types and classes relevant for the protocol definition itself. However, they can also be used by the end application if need arises to introduce such modifications.

Defining Message Interface Class

The protocol definition is expected to define extendable message interface class pinning only serialisation endian and numeric message ID type (most probably enum).

namespace my_protocol
{
// Used IDs definition
enum MsgId : std::uint8_t
{
MsgId_Message1,
MsgId_Message2,
MsgId_Message3,
...
};
template <typename... TOptions>
using Message =
comms::option::def::MsgIdType<MsgId>, // type of message ID
TOptions...
>;
} // namespace my_protocol
Main interface class for all the messages.
Definition Message.h:80
Endian< comms::traits::endian::Big > BigEndian
Alias option to Endian specifying Big endian.
Definition options.h:177
Option used to specify type of the ID.
Definition options.h:187

Such interface class is NOT polymorphic, it defines the following inner types

namespace my_protocol
{
class Message : public comms::Message
// comms::Field class with the same endian option.
// Can (and should) be provided as a base class to all the
// fields.
typedef comms::Field<.../* Same endian option*/> Field;
// Type of the ID, same as the one passed with comms::option::def::MsgIdType
typedef MsgId MsgIdType;
// Type of the ID, when it is passed as a parameter and/or returned from the function:
typedef MsgId MsgIdParamType;
};
} // namespace my_protocol
Base class to all the field classes.
Definition Field.h:33

Note the existence of MsgIdType and MsgIdParamType. When the type used for message ID is simple integral one or enum, these types are equal. However, if some other type is used, such as std::string, then MsgIdParamType is a const-reference to MsgIdType.

The message interface class may be extended with multiple options, which automatically add virtual functions, and hence create polymorphic behaviour relevant to the client application.

In general, all the API functions that are being added to the interface (and described below) use Non-Virtual Interface idiom:

class MyMessage
{
public:
void someFunction(...)
{
...; // Pre-conditions check and/or other common operations
someFunctionImpl(...); // Invocation of polymorphic functionality
...; // Post-conditions check and/or other common operations
}
protected:
virtual void someFunctionImpl(...) = 0; // Must be implemented in the derived class
};

The polymorphic behaviour is exposed via protected virtual functions having the same name, but with Impl suffix.

All the variants of message interface class that are going to be described below are descendants of comms::Message class. Please refer to the documentation of the latter for detailed info on the described functions and their parameters.

Polymorphic Retrieval of Message ID

When there is a need to be able to polymorphically retrieve message ID, the comms::option::app::IdInfoInterface option needs to be used. Note, that this option requires presence of comms::option::def::MsgIdType (which is expected to be used in protocol definition) to specify type of the message ID in order to work properly.

using MyMessage =
my_protocol::Message<
...
>;
Option used to add getId() function into Message interface.
Definition options.h:1266

It adds the following functions:

class MyMessage
{
public:
// API function to retrieve ID of the function
MsgIdParamType getId() const
{
return getIdImpl();
}
protected:
virtual MsgIdParamType getIdImpl() const = 0; // Automatically implemented in the actual message class
}
virtual MsgIdParamType getIdImpl() const =0
Pure virtual function used to retrieve ID of the message.
MsgIdParamType getId() const
Retrieve ID of the message.

The usage of comms::option::app::IdInfoInterface without comms::option::def::MsgIdType will be ignored, and getId() as well as getIdImpl() member functions won't be created.

The comms::Message interface class defines comms::Message::hasGetId() static constexpr member function, which may be used at compile time to determine whether the comms::option::app::IdInfoInterface option has been used, i.e. the message interface class defines mentioned earlier functions.

Polymorphic Read of Payload (Deserialisation)

If the implementation requires polymorphic read and process of input messages, the read() operation needs to be added to the interface. It is achieved by using comms::option::app::ReadIterator option to provide a type of the iterator that is going to be used for reading:

using MyMessage =
my_protocol::Message<
...
...
>;
Option used to specify type of iterator used for reading.
Definition options.h:1256

As the result the interface class defines the following types and functions:

class MyMessage
{
public:
// Type of the the iterator used for reading, the same as provided with
// comms::option::app::ReadIterator option.
typedef ... ReadIterator;
// API function to perform read
comms::ErrorStatus read(ReadIterator& iter, std::size_t len)
{
return readImpl(iter, len);
}
protected:
// Expected to be overriden in the derived class.
virtual comms::ErrorStatus readImpl(ReadIterator& iter, std::size_t len)
{
}
}
TypeProvidedWithOption ReadIterator
Type of the iterator used for reading message contents from sequence of bytes stored somewhere.
Definition Message.h:230
ErrorStatus read(ReadIterator &iter, std::size_t size)
Read message contents using provided iterator.
virtual comms::ErrorStatus readImpl(ReadIterator &iter, std::size_t size)
Virtual function used to implement read operation.
ErrorStatus
Error statuses reported by the Communication module.
Definition ErrorStatus.h:17
@ NotSupported
The operation is not supported.

Please note, that COMMS library doesn't impose any restrictions on how the input data is collected and stored. It is a responsibility of the caller to allocate and maintain the input buffer, while providing only an iterator for read operation.
Also note, that iterator is passed by reference, which allows advancing operator when read operation is performed.
For example:

std::size_t readMessage(MyMessage& msg, const std::uint8_t* buf, std::size_t len)
{
MyMessage::ReadIterator readIter = buf;
auto es = msg.read(readIter, len); // readIter is advanced in the read operation
... // Report and handle error
return 0U;
}
// Report number of processed bytes from buffer:
auto bytesCount = std::distance(MyMessage::ReadIterator(buf), readIter);
return bytesCount;
}
@ Success
Used to indicate successful outcome of the operation.

The comms::Message interface class defines comms::Message::hasRead() static constexpr member function, which may be used at compile time to determine whether the comms::option::app::ReadIterator option has been used, i.e. the message interface class defines mentioned earlier types and functions.

Polymorphic Write of Payload (Serialisation)

If the implementation requires polymorphic serialisation of the messages and sending them over I/O link, the write() operation needs to be added to the interface. It is achieved by using comms::option::app::WriteIterator option to provide a type of the iterator that is going to be used for writing:

using MyMessage =
my_protocol::Message<
...
...
>;
Option used to specify type of iterator used for writing.
Definition options.h:1262

As the result the interface class defines the following types and functions:

class MyMessage
{
public:
// Type of the the iterator used for writing, the same as provided with
// comms::option::app::WriteIterator option.
typedef ... WriteIterator;
// API function to perform write
comms::ErrorStatus write(WriteIterator& iter, std::size_t len)
{
return writeImpl(iter, len);
}
protected:
// Expected to be overriden in the derived class.
virtual comms::ErrorStatus writeImpl(WriteIterator& iter, std::size_t len)
{
}
}

Please note, that COMMS library doesn't impose any restrictions on storage type for the output buffer. It is a responsibility of the caller to allocate and maintain the output buffer, while providing only an iterator for write operation. In the example above the output buffer is chosen to be std::vector<std::uint8_t> and the write operation will be performed using push_back() calls on this vector (due to std::back_insert_iterator being chosen as WriteIterator).

Also note, that iterator is passed by reference, which allows advancing operator when write operation is performed. In case the iterator is random-access one, the difference between the initial and its value after the write has been performed can be used to determine amount of bytes that have been written to the buffer.

The comms::Message interface class defines comms::Message::hasWrite() static constexpr member function, which may be used at compile time to determine whether the comms::option::app::WriteIterator option has been used, i.e. the message interface class defines mentioned earlier types and functions.

Polymorphic Serialisation Length Retrieval

Sometimes it may be needed to polymorphically retrieve the serialisation length of the message in order to be able to reserve or allocate enough space for output buffer. The COMMS library provides comms::option::app::LengthInfoInterface option that adds length() member function to the interface.

using MyMessage =
my_protocol::Message<
...
...
>;
Option used to add length() function into Message interface.
Definition options.h:1274

This option adds the following functions to the interface definition:

class MyMessage
{
public:
// Retrieve the serialisation length
std::size_t length() const
{
return lengthImpl();
}
protected:
virtual std::size_t lengthImpl() const {...}; // Must be overridden in the derived class
};

The comms::Message interface class defines comms::Message::hasLength() static constexpr member function, which may be used at compile time to determine whether the comms::option::app::LengthInfoInterface option has been used, i.e. the message interface class defines mentioned earlier functions.

Polymorphic Validity Check

Sometimes it may be needed to be able to check whether the message contents (fields) have valid values. The COMMS library provides comms::option::app::ValidCheckInterface option that adds valid() member function to the interface:

using MyMessage =
my_protocol::Message<
...
...
>;
Option used to add valid() function into Message interface.
Definition options.h:1270

This option adds the following functions to the interface definition:

class MyMessage
{
public:
// Retrieve the serialisation length
bool valid() const
{
return validImpl();
}
protected:
virtual bool validImpl() const
{
return true; // By default all messages are valid, can be overridden in derived class.
}
};

The comms::Message interface class defines comms::Message::hasValid() static constexpr member function, which may be used at compile time to determine whether the comms::option::app::ValidCheckInterface option has been used, i.e. the message interface class defines mentioned earlier functions.

Polymorphic Dispatch Message for Handling

When new data arrives on I/O link, it's transport framing is going to be processed (described in detailed in Transport Framing section below). and new message object is going to be created. It's going to be returned as a smart pointer (std::unique_ptr) to the defined interface class (MyMessage). The actual type of this message object needs to be recognised and message properly handled. Using simple switch statement on message ID (returned by getId() interface function) can result in significant amount of boilerplate code, which grows and must be updated every time new message is added to the protocol. The COMMS library provides much better way to dispatch messages to appropriate handler.

The handler class needs to be forward declared and passed to the definition of MyMessage interface via comms::option::app::Handler option.

// Forward declaration
class MyHandler;
using MyHandler =
my_protocol::Message<
...
...
>;
Option used to specify type of the message handler.
Definition options.h:1288

When this option is used the MyMessage will define the following interface types and functions:

class MyMessage
{
public:
// The same type as passed via comms::option::app::Handler option
typedef ... Handler;
// Return type of the dispatch function, which is the same as return type of
// every Handler::handle() member function
typedef ... DispatchRetType;
// Dispatch this message to handler
DispatchRetType dispatch(Handler& handler)
{
return dispatchImpl(handler);
}
protected:
virtual DispatchRetType dispatchImpl(Handler& handler) {...} // Must be overridden in the derived class
};

More details about polymorphic dispatching and handling will be provided below in Message Handling section.

The comms::Message interface class defines comms::Message::hasDispatch() static constexpr member function, which may be used at compile time to determine whether the comms::option::app::Handler option has been used, i.e. the message interface class defines mentioned earlier types and functions.

Keeping Message Contents in a Consistent State

Some communication protocol may define fields that depend on other fields. For example, bits in a bitmask field may be used to define whether some optional fields exist. Or the information about amount of elements in the list to follow may reside in an independent numeric field.
After updating such fields directly, using the interface of the message object, the message contents may end up being in an inconsistent (or invalid) state. There may be a need to polymorphically normalise the state of the message object. The COMMS library provides comms::option::app::RefreshInterface option, that adds refresh() member function to the message interface.

using MyMessage =
...
...
>;
Option used to add refresh() function into Message interface.
Definition options.h:1278

This option adds the following functions to the interface definition:

class MyMessage
{
public:
// Refresh message contents
bool refresh()
{
return refreshImpl();
}
protected:
virtual bool refreshImpl()
{
return false;
}
};

Note, that the refresh() member function returns boolean value, which is expected to be true in case at least one of the internal fields has been updated, and false if message state remains unchanged.
Also note, that interface provide default implementation of refreshImpl() virtual function. The message object that require proper "refresh" functionality may just override it with proper implementation.

The comms::Message interface class defines comms::Message::hasRefresh() static constexpr member function, which may be used at compile time to determine whether the comms::option::app::RefreshInterface option has been used, i.e. the message interface class defines mentioned earlier functions.

Polymorphic Message Name Retrieval

Some applications may require knowledge about and report the name of the received / sent message. The COMMS library provides comms::option::app::NameInterface option, that adds name() member function to the message interface (see comms::Message::name()).

using MyMessage =
...
...
>;
Option used to add name() function into Message interface.
Definition options.h:1282

This option adds the following functions to the interface definition:

class MyMessage
{
public:
// Retrieve name of the message
const char* name() const
{
return nameImpl();
}
protected:
virtual const char* nameImpl() const = 0; // Must be overridden in the derived class
};

The comms::Message interface class defines comms::Message::hasName() static constexpr member function, which may be used at compile time to determine whether the comms::option::app::NameInterface option has been used, i.e. the message interface class defines mentioned earlier functions.

Virtual Destructor

By default the comms::Message class defines its destructor as virtual if and only if it exhibits a polymorphic behaviour, i.e. if there is at least one other virtual function defined. There are a couple of ways to change this default behaviour.

  • If the definition of the common message interface class using exhibits polymorphic behaviour (i.e. has other virtual functions), but mustn't define its destructor as virtual, use comms::option::app::NoVirtualDestructor option in the interface class definition.
    using MyMessage =
    my_protocol::Message<
    ...,
    >;
    Force the destructor of comms::Message class to be non-virtual, even if there are other virtual funct...
    Definition options.h:1377
  • If the definition of the common interface class doesn't have any virtual function, but still requires an ability to be polymorphically deleted, i.e. must have virtual destructor, just inherit from my_protocol::Message and define the destructor as virtual.
    class MyMessage : public
    my_protocol::Message<>
    {
    public:
    virtual ~MyMessage() = default;
    };

Interface Options Summary

All the options introduced above can be used in any order. They can also be repeated multiple times. However, the option that was defined first takes priority over (or overrides) the same option defined later.
For example, the definition below defines WriteIterator to be std::uint8_t*, because it was defined with first comms::option::app::WriteIterator option:

The definition below gives a full interface of all the introduced functions: getId(), read(), write(), dispatch(), length(), valid(), refresh(), and name().

using MyMessage = my_protocol::Message<
comms::option::app::IdInfoInterface, // Add an ability to retrieve message ID value
comms::option::app::ReadIterator<const std::uint8_t*>, // Use const std::uint8_t* as iterator for reading
comms::option::app::WriteIterator<std::uint8_t*>, // Use std::uint8_t* as iterator for writing
comms::option::app::Handler<MyHandler>, // My MyHandler class declared earlier as a handler for messages
comms::option::app::LengthInfoInterface, // Add an ability to retrieve serialisation length
comms::option::app::ValidCheckInterface, // Add an ability to check contents validity
comms::option::app::RefreshInterface, // Add an ability to refresh message contents
comms::option::app::NameInterface // Add an ability to retrieve message name
>;

In case no polymorphic interface extension option has been chosen, every message object becomes a simple "data structure" without any v-table "penalty".

using MyInterface = my_protocol::Message<>;

Protocol Messages

The protocol messages are expected to be defined as template classes, receiving at least one template parameter, which specifies the application specific interface class. For example

namespace my_protocol
{
namespace message
{
template <typename TBase>
class Message1 : public
TBase,
...
>
{
...
};
} // namespace message
} // namespace my_protocol
Base class for all the custom protocol messages.
Definition MessageBase.h:83

The interface class that was defined for the application (MyMessage) needs to be passed as TBase template parameter. The defined message class extends comms::MessageBase, which in turn extends provided interface class TBase, which in turn extends (or typedef-s) comms::Message. The inheritance hierarchy may look like this:

Due to the fact that every protocol message class extends comms::MessageBase, the detailed documentation on available member types and functions can be viewed on comms::MessageBase reference page.

All the protocol message classes implement non-virtual functions that may be used to implement polymorphic behavior. These function has the same name as described earlier interface, but start with do* prefix.

Based on the requested polymorphic functionality, the comms::MessageBase class automatically implements virtual *Impl() member functions (but only when needed).

namespace my_protocol
{
namespace message
{
template <typename TBase>
class Message1 : public
TBase,
...
>
{
public:
template <typename TIter>
comms::ErrorStatus doRead(TIter& iter, std::size_t len) {...}
template <typename TIter>
comms::ErrorStatus doWrite(TIter& iter, std::size_t len) const {...}
...
protected:
virtual comms::ErrorStatus readImpl(ReadIterator& iter, std::size_t len)
{
return doRead(iter, len);
}
virtual comms::ErrorStatus writeImpl(WriteIterator& iter, std::size_t len) const
{
return doWrite(iter, len);
}
...
};
} // namespace message
} // namespace my_protocol

Such architecture allows usage of non-virtual functions when actual type of the message is known. For example

template <typename TMsg>
void writeMessage(const TMsg& msg)
{
auto es = msg.doWrite(...);
...
}

and using polymorphic behaviour when not

void writeMessage(const MyMessage& msg)
{
static_assert(MyMessage::hasWrite(), "MyMessage must support polymorphic write");
auto es = msg.write(...);
...
}

Every message has zero or more fields, which are stored in std::tuple as private members of comms::MessageBase. The access to the fields can be obtained using fields() member function (see comms::MessageBase::fields()).

However, every message that has at least one field is expected to use COMMS_MSG_FIELDS_NAMES() (or COMMS_MSG_FIELDS_ACCESS() in older versions) macro to provide names to inner fields.

namespace my_protocol
{
namespace message
{
template <typename TBase>
class Message1 : public
TBase,
...
>
{
using Base = comms::MessageBase<...>;
public:
COMMS_MSG_FIELDS_NAMES(value1, value2, value3);
...
};
} // namespace message
} // namespace my_protocol

It is equivalent of having the following types and member functions defined.

namespace my_protocol
{
namespace message
{
template <typename TBase>
class Message1 : public
TBase,
...
>
{
public:
enum FieldIdx
{
FieldIdx_value1,
FieldIdx_value2,
FieldIdx_value3,
FieldIdx_numOfValues
}
// Access the "value1" field
auto field_value1() -> decltype(std::get<FieldIdx_value1>(fields()))
{
return std::get<FieldIdx_value1>(fields());
}
// Access the "value1" field (const variant)
auto field_value1() const -> decltype(std::get<FieldIdx_value1>(fields()))
{
return std::get<FieldIdx_value1>(fields());
}
// Access the "value2" field
auto field_value2() -> decltype(std::get<FieldIdx_value2>(fields()))
{
return std::get<FieldIdx_value2>(fields());
}
// Access the "value2" field (const variant)
auto field_value2() const -> decltype(std::get<FieldIdx_value2>(fields()))
{
return std::get<FieldIdx_value2>(fields());
}
// Access the "value3" field
auto field_value3() -> decltype(std::get<FieldIdx_value3>(fields()))
{
return std::get<FieldIdx_value3>(fields());
}
// Access the "value3" field (const variant)
auto field_value3() const -> decltype(std::get<FieldIdx_value3>(fields()))
{
return std::get<FieldIdx_value3>(fields());
}
// Types of used fields
using Field_value1 = ... /* implementation dependent field type */
using Field_value2 = ... /* implementation dependent field type */
using Field_value3 = ... /* implementation dependent field type */
};
} // namespace message
} // namespace my_protocol
STL namespace.

As the result every message field can be accessed by index

using MyMessage1 = my_protocol::Message1<MyMessage>;
MyMessage1 msg;
auto& msg1Fields = msg.fields(); // access to std::tuple of message fields.
auto& value1Field = std::get<MyMessage1::FieldIdx_value1>(msg1Fields);
auto& value2Field = std::get<MyMessage1::FieldIdx_value2>(msg1Fields);
auto& value2Field = std::get<MyMessage1::FieldIdx_value2>(msg1Fields);

or by name

auto& value1Field = msg.field_value1();
auto& value2Field = msg.field_value2();
auto& value2Field = msg.field_value3();

Message Fields

In order to continue with the tutorial, it is paramount to understand a concept of fields, which 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;
// 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() {...}
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
bool valid() const
Check validity of the field value.
Definition IntValue.h:287
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
const ValueType & value() const
Get access to integral value storage.
Definition IntValue.h:227
bool refresh()
Refresh the field's value.
Definition IntValue.h:294

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. It may be inner Field type of MyMessage interface class defined earlier (MyMessage::Field - documented as comms::Message::Field)
  • exhibits some default behaviour which can be modified by passing various options from comms::option::app and/or comms::option::def namespaces as additional template parameters. The options that define how field is serialised are expected to be used as part of protocol definition. The protocol definition is also expected to allow passing extra options that are relevant to application environment and/or behaviour (such as modifying the default storage type).
  • 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.

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

The most important member function to a client application is value(). It allows access to the stored value. Note, that the stored value is accessed by reference. It allows both get and set operations:

auto myFieldValue = myField.value();
myField.value() = 5U;

Other member functions are of lesser importance to the client application, they are used by the protocol definition itself to properly (de)serialise message contents and provide other useful functionality.

The available fields abstractions are:

Integral Value Fields

Integral value fields are defined using comms::field::IntValue class. Its inner ValueType type is the same as second template parameter. Most integral value fields are defined and used "as-is"

// base class for all the fields, usually defined by the protocol definition library
// definition of integral field
using MyIntField =
MyFieldBase, // base class for all the fields, defined by the protocol definition library
std::uint16_t
>;
// usage of the field
MyIntField field;
field.value() = 5; // serialised as "00 05"

Some field's definitions may use comms::option::def::NumValueSerOffset option, which adds predefined offset to the field before serialising and subtracts it before deserialising. Classic example would be having a "year" information, but serialised as offset from year 2000 with a single byte. Such field may be defined as following:

using YearField =
MyFieldBase,
std::int16_t,
>;
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

NOTE, that while serialisation takes only 1 byte, the client application will use full year number without worrying about added / removed offset

YearField field;
field.value() = 2018; // serialised as 0x12;

Some protocols may exchange floating point values by serialising them as integral ones. For example, multiply the floating point value by 1000 before the serialisation, and upon reception divide the received value by 1000 to get the floating point one. Such fields will be defined using comms::field::IntValue type with using comms::option::def::ScalingRatio option

using MyFpField =
MyFieldBase, // base class for all the fields, defined by the protocol definition library
std::int32_t,
>;
Option to specify scaling ratio.
Definition options.h:410

The inner value of such field is integral one. However, there are comms::field::IntValue::getScaled() and comms::field::IntValue::setScaled() member functions that allow get and set original floating point value without worrying what math operation needs to be performed.

MyFpField field;
field.setScaled(1.3f);
assert(field.value() == 1300);
auto asDouble = field.getScaled<double>(); // equivalent to 1.3

In addition to scaling, the COMMS library also provides an ability to specify units. For example, some protocol may define distance in 1/10 of the millimetres. The definition of such field may look like this

using MyDistance =
MyFieldBase,
std::int32_t,
Options to specify units of the field.
Definition options.h:693

The COMMS library provides a limited set of units conversion functions in comms::units namespace. When using the provided conversion function the application developer doesn't need to remember the original units and/or scaling factor. The COMMS library does all the math. It also prevents (at compile time) usage of wrong conversion functions, say calculating time (milliseconds), when specified units are distance (millimetres).

MyDistance field;
comms::units::setMeters(field, 1.2345);
std::cout << "Distance in 1/10 of mm:" << field.value() << std::endl; // prints 12345
std::cout << "Distance in mm" << comms::units::getMillimeters<double>(field); // prints 1234.5
std::cout << "Distance in cm" << comms::units::getCentimeters<double>(field); // prints 123.45
void setMeters(TField &field, TVal &&val)
Update field's value accordingly, while providing meters value.
Definition units.h:1178

NOTE, that COMMS library is about the communication protocols and not about unit conversions. The unit conversion functionality is quite basic and limited. If there is a need to use third party unit conversion library, it could be wise to static_assert on assumption for origin units.

static_assert(comms::units::isMillimeters<MyDistance>(), "Invalid units assumption");

or

static_assert(comms::units::isMillimeters(field), "Invalid units assumption");
constexpr bool isMillimeters()
Compile time check whether the field type holds millimeters.
Definition units.h:1080

By default, When comms::field::IntValue field is constructed, the inner value is constructed to be 0. However, the field definition may use comms::option::def::DefaultNumValue option to specify some other value

// definition of integral field
using MyIntField =
MyFieldBase, // base class for all the fields, defined by the protocol definition library
std::uint16_t,
>;
MyIntField field;
assert(field.value() == 25);
Option that specifies default initialisation class.
Definition options.h:616

Enum Value Fields

The enum values are defined using comms::field::EnumValue class. It is very similar to Integral Value Fields. The main difference, that second template parameter as well as inner ValueType type is enum. The enum can be scoped (enum class) or regular (just enum).

enum class SomeEnumVal : std::uint8_t
{
Val1,
Val2,
Val3,
NumOfValues
};
using SomeEnumField =
MyFieldBase,
SomeEnumVal,
comms::option::def::ValidNumValueRange<0, (int)SomeEnumVal::NumOfValues - 1>,
comms::option::def::DefaultNumValue<(int)SomeEnumVal::Val3>
>;
SomeEnumField field;
assert(field.value() == SomeEnumVal::Val3); // initialised by default to Val3;
field.value() = SomeEnumVal::Val2;
Enumerator value field.
Definition EnumValue.h:73
Provide range of valid numeric values.
Definition options.h:956

Note, that underlying type of the enum dictates default serialisation length.

Bitmask Value Fields

Bitmasks (or bitsets) are also numeric values where every bit has separate, independent meaning. Such fields are defined using comms::field::BitmaskValue class.

struct MyBitmask : public
MyFieldBase,
comms::option::def::FixedLength<1>,
comms::option::def::BitmaskReservedBits<0x2, 0> // Second bit is reserved and must be 0
>
{
...
};
Bitmask value field.
Definition BitmaskValue.h:103

The field definition will use comms::option::def::FixedLength option in order to specify its serialisation length. The inner ValueType type will be calculated automatically and defined as one of the unsigned types: std::uint8_t, std::uint16_t, std::uint32_t, or std::uint64_t. The usage of comms::option::def::BitmaskReservedBits option will mark certain bits as "reserved". It influences only validity check functionality (see comms::field::BitmaskValue::valid()). If any of the reserved bits doesn't have an expected value, the call to valid() member function will return false.

The bitmask field definition is also expected to use COMMS_BITMASK_BITS() and COMMS_BITMASK_BITS_ACCESS() macros to provide names for the bits and generate convenience access function. If bit names are sequential, i.e. start from bit 0, and go up without any holes in the middle, then single COMMS_BITMASK_BITS_SEQ() macro can be used instead achieving the same effect.

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
#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
}
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

The generated convenience access functions use existing comms::field::BitmaskValue::getBitValue() and comms::field::BitmaskValue::setBitValue() member functions.

It is also possible to set multiple bits at the same time by accessing the stored value directly

MyBitmask field;
field.value() = 0x81; // Setting first and eighth bits

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 RS-232 serial port configuration may be defined as following:

3 Bits to configure baud rate:

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

2 Bits to configure parity:

Parity Serialisation Value
None 0
Odd 1
Even 2

2 Bits to configure stopBits:

Stop Bits Serialisation Value
One 0
One and half 1
Two 2

2 Bits to configure flow control:

Flow Control Serialisation Value
None 0
Hardware 1
Software 2

The field definition will use comms::field::Bitfield and probably look similar to code below

enum class Baud {...}
enum class Parity {...}
enum class StopBits {...}
enum class FlowControl {...}
class SerialConfigField : public
MyFieldBase,
std::tuple<
comms::field::EnumValue<MyFieldBase, Baud, comms::option::def::FixedBitLength<3> >,
comms::field::EnumValue<MyFieldBase, Parity, comms::option::def::FixedBitLength<2> >,
comms::field::EnumValue<MyFieldBase, StopBits, comms::option::def::FixedBitLength<2> >,
comms::field::EnumValue<MyFieldBase, FlowControl, comms::option::def::FixedBitLength<2> >,
comms::field::IntValue<MyFieldBase, std::uint8_t, comms::option::def::FixedBitLength<7> >
>
>
{
// (Re)definition of the base class as inner Base type.
using Base = comms::field::Bitfield<...>;
public:
COMMS_FIELD_MEMBERS_NAMES(baud, parity, stopBits, flowControl, reserved);
}
#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
Bitfield field.
Definition Bitfield.h:98

All the member fields of the comms::field::Bitfield are stored internally as std::tuple, as the result the inner ValueType of such field is std::tuple of all member fields and call to comms::field::Bitfield::value() member function will give an access to it.

The field definition is expected to use COMMS_FIELD_MEMBERS_NAMES() macro, which will generate FieldIdx enum as well as convenience access member functions. The code becomes equivalent to:

struct SerialConfigField : public
MyFieldBase,
std::tuple<
comms::field::EnumValue<MyFieldBase, Baud, comms::option::def::FixedBitLength<3> >,
comms::field::EnumValue<MyFieldBase, Parity, comms::option::def::FixedBitLength<2> >,
comms::field::EnumValue<MyFieldBase, StopBits, comms::option::def::FixedBitLength<2> >,
comms::field::EnumValue<MyFieldBase, FlowControl, comms::option::def::FixedBitLength<2> >,
comms::field::IntValue<MyFieldBase, std::uint8_t, comms::option::def::FixedBitLength<7> >
>
>
{
// Access indices for member fields
enum FieldIdx {
FieldIdx_baud,
FieldIdx_parity,
FieldIdx_stopBits,
FieldIdx_flowControl,
FieldIdx_reserved,
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());
}
... // and so on for all other member fields
}
const ValueType & value() const
Get access to the stored tuple of fields.
Definition Bitfield.h:194

Accessing the member field value in such setup, such as "baud" may look like this:

SerialConfigField configField;
configField.field_baud().value() = Baud::Val_115200;

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,
...
}
class MyBundle : public
MyFieldBase,
std::tuple<
comms::field::IntValue<MyFieldBase, std::int16_t> // 2 bytes int value
comms::field::EnumValue<MyFieldBase, SomeEnum>, // 1 byte enum value
comms::field::BitmaskValue<MyFieldBase, comms::option::def::FixedLength<1> > // 1 byte bitmask
>
>
{
// (Re)definition of the base class as inner Base type.
using Base = comms::field::Bundle<...>;
public:
COMMS_FIELD_MEMBERS_NAMES(member1, member2, member3);
};
Bundles multiple fields into a single field.
Definition Bundle.h:61

Just like with Bitfield Fields, the inner ValueType type of such field is a std::tuple of member fields, and usage of COMMS_FIELD_MEMBERS_NAMES() has exactly the same effect, i.e. generates inner FieldIdx enum and convenience access member functions:

struct MyBundle : public
MyFieldBase,
std::tuple<
comms::field::IntValue<MyFieldBase, std::int16_t> // 2 bytes int value
comms::field::EnumValue<MyFieldBase, SomeEnum>, // 1 byte enum value
comms::field::BitmaskValue<MyFieldBase, comms::option::def::FixedLength<1> > // 1 byte bitmask
>
>
{
enum FieldIdx
{
FieldIdx_member1,
FieldIdx_member2,
FieldIdx_member3,
FieldIdx_numOfValues
};
// Accessor to "member1" field
auto field_member1() -> decltype(std::get<FieldIdx_member1>(value()))
{
return std::get<FieldIdx_member1>(value());
}
// Accessor to const "member1" field
auto field_member1() const -> decltype(std::get<FieldIdx_member1>(value()))
{
return std::get<FieldIdx_member1>(value());
}
// Accessor to "member2" field
auto field_member2() -> decltype(std::get<FieldIdx_member2>(value()))
{
return std::get<FieldIdx_member2>(value());
}
...
};
ValueType & value()
Get access to the stored tuple of fields.
Definition Bundle.h:148

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 // raw byte type as second template parameter
>;
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
>;

The default storage type (inner ValueType type) of these fields is std::vector of specified type, i.e. it is std::vector<std::uint8_t> in case of MySimpleList and std::vector<MyBundle> in case of MyComplexList. Such default storage type may be unsuitable to certain applications, especially for bare-metal ones. The COMMS library allows changing it using extra options. The protocol definition is expected to provide a way to pass extra options to such fields. It is explained in Application Specific Customisation of Fields section below in more details.

Please pay attention, that in case of non-raw data lists, the inner std::vector contains fields, not values. As the result access to the stored values may require a bit of extra function calls:

MyComplexList list;
auto& storageVec = list.value(); // access to std::vector<MyBundle>
storageVec.resize(1);
auto& firstBundle = storageVec[0]; // access to first MyBundle element
auto& firstMember1 = firstBundle.field_member1(); // access to "member1" member of first bundle
firstMember1.value() = ...; // assign the value of "member1"

Some protocols may define fixed size lists. In such case lists are defined with usage of comms::option::def::SequenceFixedSize option.

using MyList =
MyFieldBase,
>;
Option used to define exact number of elements in the collection field.
Definition options.h:579

Usage of this option just ensures right amount of elements "on the wire" after the field is serialised, but it does NOT automatically resize inner storage vector.

MyList field;
assert(field.value().empty()); // vector is empty upon construction

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.

However, there are options that can modify this default behaviour. The protocol definition classes are expected to provide a way to pass extra application specific options to the string field definition. It is explained in more details in Application Specific Customisation of Fields section below.

Also similar to Array List Fields, fixed length strings are defined using comms::option::def::SequenceFixedSize option, and just like with lists it does NOT automatically resize inner string, just ensures right amount of characters "on the wire" when field is serialised.

using MyFixedString =
MyFieldBase,
>;
MyFixedString field;
assert(field.value().empty());

Floating Point Value Fields

Floating point value fields are defined using comms::field::FloatValue They 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 the same scaling and units conversion just like Integral Value Fields.

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.

using OptField =
>;
Adaptor class to any other field, that makes the field optional.
Definition Optional.h:45

The default mode of such field is "tentative", which means read if there is data available in the input buffer, and write if there is enough space in the output buffer.

OptField field;
assert(field.isTentative());

The default mode can be changed using comms::option::def::ExistsByDefault or comms::option::def::MissingByDefault options. For example

using ExistingOptField =
>;
ExistingOptField field;
assert(field.doesExist());

NOTE, that inner ValueType of such field is wrapped actual field and both comms::field::Optional::value() and comms::field::Optional::field() member functions allow to access it. For example:

OptField field;
auto& innerField = field.field(); // or call field.value()
innerField.value() = 1234;
field.setExists();

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 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. Such fields are defined using comms::field::Variant class. It is very similar to Bundle Fields, but serves as one big union of provided member fields, i.e. only one can be used at a time.

As an example for the key/value pairs let's assume usage of 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 and the protocol definition may look something like this:

// Definition of value fields
using Value3 =
MyFieldBase,
MyFieldBase,
std::uint8_t
>
>
>;
//The common key type is usually represented as enum.
enum class KeyId : std::uint8_t
{
Key1,
Key2,
Key3,
NumOfValues
};
// Definition of "key" fields
template <KeyId TId>
using KeyField =
MyFieldBase,
KeyId,
>;
using Key1 = KeyField<KeyId::Key1>;
using Key2 = KeyField<KeyId::Key2>;
using Key3 = KeyField<KeyId::Key3>;
// Definition of properties bundles
template <typename TKey, typename TValue>
class Property : public
MyFieldBase,
std::tuple<
TKey,
TValue
>
>
{
using Base = ...; // repeat base definition
public:
};
using Property1 = Property<Key1, Value1>;
using Property2 = Property<Key2, Value2>;
using Property3 = Property<Key3, Value3>;
// Definition of the variant field
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);
};
// Definition of properties list
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:929
Option that forces field's read operation to fail if invalid value is received.
Definition options.h:674
Option that modifies the default behaviour of collection fields to prepend the serialised data with n...
Definition options.h:438

IMPORTANT: The comms::field::Variant field uses uninitialized implementation dependent storage area able to be initialized and hold any of the provided member field types (but only one at a time). The inner ValueType type contains definition of the used storage type and value() member function which returns reference to the used storage area. These type and function should NOT be used directly. Instead, the protocol definition is expected to use COMMS_VARIANT_MEMBERS_NAMES() macro, which generates appropriate access and conversion wrapper functions around comms::field::Variant::initField() and comms::field::Variant::accessField().

The usage of COMMS_VARIANT_MEMBERS_NAMES() in the above is equivalent to having the following member types and functions defined

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>();
}
// Types of used fields
using Field_prop1 = ... /* implementation dependent field type */
using Field_prop2 = ... /* implementation dependent field type */
using Field_prop3 = ... /* implementation dependent field type */
};

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

When variant field object is instantiated, accessing the currently held field can be tricky though. There is a need to differentiate between compile-time and run-time knowledge of the contents.

When preparing a variant field (or message with variant fields) to be sent out, usually the inner field type and its value are known at compile time. The initialization of the field can be performed using one of the initField_*() member function described above:

MyVariant var; // Created in "invalid" state
auto& prop1 = var.initField_prop1(); // Initialise as Property1 (constructor of prop1 is called)
auto& prop1ValField = prop1.field_val(); // Access the "value" field of the bundle
prop1ValField.value() = 0xff; // Update the property value

or use comms::field::Variant::initField() member function and generated FieldIdx enum as compile time access index:

auto& prop1 = var.initField<MyVariant::FieldIdx_prop1>();

It is possible to re-initialize the field as something else, the previous definition will be properly destructed.

MyVariant var; // Created in "invalid" state
auto& prop1 = var.initField_prop1(); // Initialize as Property1 (constructor of prop1 is called)
auto& prop2 = var.initField_prop2(); // Destruct Property1 and initialize as Property2

If the variant field has been initialized before, but there is a need to access the real type (also known at compile time), use appropriate accessField_*() member function:

void updateProp1(MyVariant& var)
{
auto& prop1 = var.accessField_prop1(); // Access as Property1 (simple cast, no call to the constructor)
auto& prop1ValField = prop1.field_val()); // Access the "value" field of the bundle
prop1ValField.value() = 0xff; // Update the property value
}

or use comms::field::Variant::accessField() member function and generated FieldIdx enum as compile time access index:

auto& prop1 = var.accessField<MyVariant::FieldIdx_prop1>();

There are cases (such as handling field after "read" operation), when actual type of the Variant field is known at run-time. The most straightforward way is to inquire the actual type index using comms::field::Variant::currentField() function and then use a switch statement and handle every case accordingly.

void handleMyVariant(const MyVariant& var)
{
switch(var.currentField())
{
case MyVariant::FieldIdx_prop1:
{
auto& prop1 = var.accessField_prop1(); // cast to "prop1"
... // handle prop1;
break;
}
case MyVariant::FieldIdx_prop2:
{
auto& prop2 = var.accessField_prop2(); // cast to "prop2"
... // handle prop2;
break;
}
...
};
}

However, such approach may require a significant amount of boilerplate code with manual (error-prone) "casting" to appropriate field type. The COMMS library provides a built-in way to perform relatively efficient (O(log(n)) way of dispatching the actual field to its appropriate handling function by using comms::field::Variant::currentFieldExec() member function. It expects to receive a handling object which can handle all of the available inner types:

struct MyVariantHandler
{
template <std::size_t TIdx>
void operator()(Property1& prop) {...}
template <std::size_t TIdx>
void operator()(Property2& prop) {...}
template <std::size_t TIdx>
void operator()(Property3& prop) {...}
}
void handleVariant(MyVariant& var)
{
var.currentFieldExec(MyVariantHandler());
}

NOTE, that every operator() function receives a compile time index of the handed field within a containing tuple. If it's not needed when handling the member field, just ignore it or static_assert on its value if the index's value is known.

The class of the handling object may also receive the handled member type as a template parameter

struct MyVariantHandler
{
template <std::size_t TIdx, typename TField>
void operator()(TField& prop) {...}
}

The example above covers basic key/value pairs type of properties. Quite often protocols use TLV (type/length/value) triplets instead. Adding length information allows having multiple value fields to follow (some of them may be introduced in future versions of protocols) as well as receiving unknown (to earlier versions of the protocol) properties and skipping over them. The definition above may be slightly altered (see below) to support such properties:

...
// No need for length prefix for string any more
...
// Definition of properties bundles
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, val);
};
...

The rest of the handling code presented above applies for this kind as well with one small nuance. The value of the length field depends on the value of val (especially with variable length fields like strings).

When such property field is default constructed, the length is updated to a correct value.

MyVariant var;
auto& prop1 = var.initField_prop1(); // Initialize as Property1 (1 byte integral value)
assert(prop1.field_length().value() == 1U);
prop1.deinitField_prop1(); // Must be de-initialized before re-initialization as something else
auto& prop3 = var.initField_prop3(); // Re-initialize as Property3 (empty string)
assert(prop3.field_length().value() == 0U); // Remaining length of empty string

In case the val field of the prop3 gets updated, the value of length field is not valid any more. There is a need to bring it into a consistent state by calling refresh() member function.

prop3.field_val().value() = "hello";
prop3.refresh();
assert(prop3.field_length().value() == 5U);

Note, that there is no need to call refresh() after every update of a variant field. Usually such updates are done as preparation of the message to be sent. It is sufficient to call doRefresh() member function of the message object at the end of the update.

SomeMessage msg;
auto& propsList = msg.field_propsList(); // access the properties list
auto& propsListVector = propsList.value(); // access the storage (vector);
propsListVector.resize(10); // create 10 properties (still invalid)
auto& prop1VariantField = propsListVector[0].initField_prop1(); // Initialize first as "prop1"
prop1VariantField.field_val().value() = 0xf;
auto& prop3VariantField = propsListVector[1].initField_prop3(); // Initialize second as "prop3"
prop3VariantField.field_val().value() = "hello";
...
msg.doRefresh(); // Bring all fields into a consistent state in one go

Application Specific Customisation of Fields

The COMMS library provides multiple options to modify default behaviour of field classes. Many of them modify the way how the field is (de)serialised. Such options are expected to be used in actual protocol definition code. However, there are also options that modify used data structures or field's behaviour. Such options are application specific, and the protocol definition is expected to provide a way on delivering such options to fields definitions. The recommended way for the protocol definition is to have separate struct called DefaultOptions which defines all the relevant extension options as its inner types. For example, The message definition may look like this:

#pragma once
#include "comms/comms.h"
#include "MyFieldBase.h" // Defines MyFieldBase common base class for all the fields
#include "DefaultOptions.h" // Defines DefaultOptions struct
namespace my_protocol
{
namespace message
{
template <typename TOpt = DefaultOptions>
struct Message1Fields
{
using field1 = ...;
using field2 = ...
using field3 =
MyFieldBase,
typename TOpt::message::Message1Fields::field3 // Extra option(s)
>
// bunding type
using All =
std::tuple<
field1,
field2,
field3
>;
};
template <typename TBase, typename TOpt = DefaultOptions>
class Message1 : public
TBase,
comms::option::def::StaticNumIdImpl<MsgId_Message1>,
comms::option::def::FieldsImpl<Message1Fields<TOpt>::All>, // using fields with extra options
comms::option::def::MsgType<Message1<TBase, TOpt> >
>
{
// (Re)definition of the base class
using Base = comms::MessageBase<...>;
public:
// Provide names for the fields
COMMS_MSG_FIELDS_NAMES(value1, value2, value3);
};
} // namespace message
} // namespace my_protocol
Aggregates all the includes of the COMMS library interface.

The inner structure of DefaultOptions struct is not important, but the recommended practice is to resemble the scope of fields. It may look like this

namespace my_protocol
{
struct DefaultOptions
{
struct message
{
struct Message1Fields
{
using field1 = comms::option::app::EmptyOption; // no extra functionality by default
using field2 = comms::option::app::EmptyOption; // no extra functionality by default
using field3 = comms::option::app::EmptyOption; // no extra functionality by default
};
};
};
} // my_protocol
No-op option, doesn't have any effect.
Definition options.h:1250

The application is expected to provide its own options describing struct / class if needed. The easiest way is to extend provided DefaultOptions and override selected types. For example

struct MyOptions : public my_protocol::DefaultOptions
{
struct message : public my_protocol::DefaultOptions::message
{
struct Message1Fields : public my_protocol::DefaultOptions::message::Message1Fields
{
};
};
};
Option that forces usage of embedded uninitialised data area instead of dynamic memory allocation.
Definition options.h:1346

The definition of the message(s) may look like this:

using MyMessage1 = my_protocol::Message1<MyMessage, MyOptions>;

If there is a need to pass more than one extra option to a field, these options can be bundled together is std::tuple.

struct MyOptions : public my_protocol::DefaultOptions
{
struct message : public my_protocol::DefaultOptions::message
{
struct Message1Fields : public my_protocol::DefaultOptions::message::Message1Fields
{
using field3 = std::tuple<..., ...>;
};
};
};

Customisation for Lists and Strings

As was already mentioned earlier the default storage type (ValueType) of Array List Fields is std::vector, while default storage type for String Fields is std::string. These types are suitable for most of C++ applications, but may be unsuitable for bare-metal embedded platforms. That why the protocol definition must allow additional customisation of such fields.

The COMMS library provides multiple options to change the default storage type. There is comms::option::app::FixedSizeStorage option. When passed to the comms::field::ArrayList or comms::field::String, it changes the default storage type to be comms::util::StaticVector or comms::util::StaticString respectively. These types expose the same public API as std::vector or std::string, but use pre-allocated storage area (as their private member) to store the elements / characters. Note, that the comms::option::app::FixedSizeStorage option has a template parameter, which specify number of elements (not necessarily bytes) to be stored. If the field definition already uses comms::option::def::SequenceFixedSize option to specify that number of elements is fixed, there is comms::option::app::SequenceFixedSizeUseFixedSizeStorage option which has the same effect of forcing comms::util::StaticVector or comms::util::StaticString to be storage types, but does not require repeating specification of storage area size. For example, if message type is defined to use provided DefaultOptions, then the storage type of field3 will be std::string

using MyMessage1 = my_protocol::Message1<MyMessage>;
MyMessage1 msg;
std::string& field3Str = msg.field_value3().value();

However, if message type is defined to used described earlier MyOptions, then the storage type of field3 will be comms::util::StaticString

using MyMessage1 = my_protocol::Message1<MyMessage, MyOptions>;
MyMessage1 msg;
comms::util::StaticString<32>& field3Str = msg.field_value3().value();
Replacement to std::string when no dynamic memory allocation is allowed.
Definition StaticString.h:789

NOTE, that using default std::vector / std::string or provided comms::util::StaticVector / comms::util::StaticString will involve copying of the data to these storage areas during the read operation. If the input buffer is contiguous, i.e the pointer to the last element is always greater than pointer to the first one (not some kind of circular buffer), then copying of the data may be avoided by using comms::option::app::OrigDataView option. The option will change the default storage types to be std::span<std::uint8_t> or std::string_view respectively. In case standard variants are unavalable due to used C++ standard or insufficient standard library version then comms::util::ArrayView and/or comms::util::StringView will be used instead.

NOTE, that passing comms::option::app::OrigDataView option to comms::field::ArrayList is possible only if it is list of raw data (std::uint8_t is used as element type).

struct MyOptions : public my_protocol::DefaultOptions
{
struct message : public my_protocol::DefaultOptions::message
{
struct Message1Fields : public my_protocol::DefaultOptions::message::Message1Fields
{
};
};
};
Use "view" on original raw data instead of copying it.
Definition options.h:1399

If default standard std::vector / std::string or all the provided by the COMMS library storage type are not good enough, it is possible to specify custom storage type using comms::option::app::CustomStorageType option. For example:

struct MyOptions : public my_protocol::DefaultOptions
{
struct message : public my_protocol::DefaultOptions::message
{
struct Message1Fields : public my_protocol::DefaultOptions::message::Message1Fields
{
using field3 =
boost::container::pmr::string
>;
};
};
};
Set custom storage type for fields like comms::field::String or comms::field::ArrayList.
Definition options.h:1363

Customisation for Other Fields

While allowing extra customisation for fields like list or string is a "must have" feature of the protocol definition, it may also allow additional customisation of other fields as well. For example, let's assume that field1 of previously mentioned Message1 is a numeric field, that defined the following way:

#pragma once
#include "comms/comms.h"
#include "MyFieldBase.h" // Defines MyFieldBase common base class for all the fields
#include "DefaultOptions.h" // Defines DefaultOptions struct
namespace my_protocol
{
namespace message
{
template <typename TOpt = DefaultOptions>
struct Message1Fields
{
using field1 =
MyFieldBase,
std::uint8_t,
typename TOpt::message::Message1Fields::field1, // Extra options
>;
...
};
template <typename TBase, typename TOpt = DefaultOptions>
class Message1 : public
TBase,
comms::option::def::StaticNumIdImpl<MsgId_Message1>,
comms::option::def::FieldsImpl<Message1Fields<TOpt>::All>, // using fields with extra options
comms::option::def::MsgType<Message1<TBase, TOpt> >
>
{
...
};
} // namespace message
} // namespace my_protocol

If not other options are passed as message::Message1Fields::field1 inner type of application specific options struct / class, then the inner value of field1 will be initialised to 10 upon construction and the range of valid values will be [10, 20]. Such default settings may be modified using various options. For example, making the value to be initialised to 5 and adding it to valid values may look like this:

struct MyOptions : public my_protocol::DefaultOptions
{
struct message : public my_protocol::DefaultOptions::message
{
struct Message1Fields : public my_protocol::DefaultOptions::message::Message1Fields
{
using field1 =
std::tuple<
>;
};
};
};

After these extra options are passed to the field's definition it becomes equivalent to

using field1 =
MyFieldBase,
std::uint8_t,
comms::option::def::DefaultNumValue<5>, // overrides default value 10 defined below
comms::option::def::ValidNumValue<5>, // added to previously defined range [10, 20]
>;

NOTE that COMMS library processes all the options bottom-up, i.e. starts with comms::option::def::ValidNumValueRange<10, 20> (which records initial valid range [10, 20]), then processes comms::option::def::DefaultNumValue<10> (which records default value to be 10), then processes comms::option::def::ValidNumValue<5> (which adds value 5 to the existing valid ranges), then processes comms::option::def::DefaultNumValue<5> (which changes the default value to be 5).

In case the application needs to override originally defined valid range(s) of the field, it can use comms::option::def::ValidRangesClear option, which will clear all previously defined valid ranges and will start accumulating them anew. For example:

struct MyOptions : public my_protocol::DefaultOptions
{
struct message : public my_protocol::DefaultOptions::message
{
struct Message1Fields : public my_protocol::DefaultOptions::message::Message1Fields
{
using field1 =
std::tuple<
comms::option::def::DefaultNumValue<5>, // change the default value
comms::option::def::ValidNumValue<5>, // add 5 to valid ranges of [40, 50]
comms::option::def::ValidRangesClear // clear the default ranges
>;
};
};
};
Clear accumulated ranges of valid values.
Definition options.h:961

Additional option that may be quite useful is comms::option::def::FailOnInvalid. It causes the read operation to fail with provided error status when read value is not valid (valid() member function returns false).

There may be other useful options, which are not covered by this tutorial, please open reference page of a required field class for the list of supported options and read their documentation for more details.

Field Value Assignment

As was mentioned earlier, every field class has value() member function, which provides an access to internal value storage. Quite often there may be case when explicit cast is required.

using MyField = comms::field::IntValue<FieldBase, std::uint8_t>; // One byte int
int someValue = ...;
MyField field;
field.value() = static_cast<std::uint8_t>(someValue);

The code above is boilerplate one, which in general should be avoided, because in case of the field definition being changed, the cast below must also be changed. Such boilerplate code can be avoided by using extra compile time type manipulations:

field.value() = static_cast<typename std::decay<decltype(field.value())>::type>(someValue);

However, it's too much typing to do and quite inconvenient for the developer. The COMMS library provides comms::cast_assign() stand alone function which can be used for easy assignments with implicit static_cast to appropriate type. It should be used for any arithmetic and/or enum storage values.

comms::cast_assign(field.value()) = someValue; // static_cast is automatic
details::ValueAssignWrapper< T > cast_assign(T &value)
Helper function to assign value with static_cast to appropriate type.
Definition cast.h:29

Cast Between Fields

There may also be cases when value of one field needs to be assigned to value of another type. If static_cast between the values works, then it is possible to use comms::cast_assign() function described in Field Value Assignment section above.

comms::assign(field1.value()) = field2.value();

However, there may be cases when such cast is not possible. For example value of 1 byte comms::field::IntValue needs to be assigned to a comms::field::Bitfield length of which is also 1 byte, but it splits the value into a couple of inner members. In this case the comms::field_cast() function should be used.

using MyBitfield = comms::field::Bitfield<...>;
MyInt field1;
MyBitfield field2;
... // Set values of field1 and field2
field2 = comms::field_cast<MyBitfield>(field1);

NOTE, that casting and assignment is performed on the field objects themselves, not their stored values.

Transport Framing

In addition to definition of the messages and their contents, every communication protocol must ensure that the message is successfully delivered over the I/O link to the other side. The serialised message payload must be wrapped in some kind of transport information prior to being sent and unwrapped on the other side when received. The COMMS protocol defines multiple so called layers (defined in comms::protocol namespace). The transport framing will be defined using those layer classes and may be called ProtocolStack or Frame. Its definition is expected to look something like this:

namespace my_protocol
{
template <
typename TMessage, // common interface class defined by the application
typename TInputMessages = AllMessages<TMessage>, // Input messages that need to be recognised
typename TAllocationOptions = comms::option::app::EmptyOption, // Extra options for comms::protocol::MsgIdLayer
typename TPayloadOptions = comms::option::app::EmptyOption // Extra options for payload storage
>
struct ProtocolStack : public
MySyncPrefix<TMessage, TInputMessages, TAllocationOptions, TPayloadOptions>
{
COMMS_PROTOCOL_LAYERS_ACCESS(...);
};
} // namespace my_protocol

The first template parameter (TMessage) is common message interface class described earlier in Defining Message Interface Class section.

The second template parameter (TInputMessages) is std::tuple of input messages that read operation is expected to recognise. The message classes must be sorted by the ascending order of message IDs. The protocol definition is also expected to define AllMessages tuple which lists all the protocol messages, which can be used as reference.

The third template parameter (TAllocationOptions) is extra option(s) (bundled in std::tuple if more than one) to be passed to comms::protocol::MsgIdLayer class which is responsible for message allocation. By default the message object is dynamically allocated, it is possible to modify such behaviour by using comms::option::app::InPlaceAllocation option. It will be explained in more details further below.

The fourth template parameter (TPayloadOptions) is irrelevant in most cases. It has a meaning only when transport wrapping values are cached for further analysis. It will also be explained in more details further below.

NOTE, that ProtocolStack definition is actually an alias to one of the classes from comms::protocol namespace. To get a detailed information on available public API please reference to one of them, for example comms::protocol::SyncPrefixLayer.

It may also happen, that the extra options (TAllocationOptions and TPayloadOptions) are defined inside the recommended DefaultOptions structure and be passed to the layers definitions themselves. Such approach is undertaken by the commsdsl2comms code generator application from the commsdsl project.

namespace my_protocol
{
struct DefaultOptions
{
...
struct frame
{
// Extra options for Layers.
struct FrameLayers
{
// Extra options for Data layer.
// Extra options for Id layer.
}; // struct FrameLayers
}; // struct frame
}/ // struct DefaultOptions
template <
typename TMessage, // common interface class defined by the application
typename TInputMessages = AllMessages<TMessage>, // Input messages that need to be recognised
typename TOpt = DefaultOptions // Options of the protocol definitions
>
struct ProtocolStack : public
MySyncPrefix<TMessage, TInputMessages, TOpt>
{
COMMS_PROTOCOL_LAYERS_ACCESS(...);
};
} // namespace my_protocol

Reading Transport Framing and Message Payload

The COMMS library provides some convenience input processing and message dispatching functionality suitable for most cases. There is comms::processAllWithDispatch() function which is responsible to process provided data in input buffer, detect and allocate message objects, and dispatch them to provided handler. For example:

#include "comms/process.h" // Contains processing function(s)
// Alias to my_protocol::ProtocolStack
using ProtStack = my_protocol::ProtocolStack<MyMessage>
ProtStack protStack; // Protocol stack object
MyHandler handler; // Handler object
std::size_t consumed = comms::processAllWithDispatch(buf, bufLen, protStack, handler);
... // Removed consumed bytes from input buffer
std::size_t processAllWithDispatch(TBufIter bufIter, std::size_t len, TFrame &&frame, THandler &handler)
Process all available input and dispatch all created message objects to appropriate handling function...
Definition process.h:246
Provides auxiliary functions for processing input and dispatching messages.

The code above will dispatch allocated message objects to the provided handler using comms::dispatchMsg() function, which in turn performs compile time analysis of supported message types and can result in either using polymorphic (see comms::dispatchMsgPolymorphic()) or static binary search (see comms::dispatchMsgStaticBinSearch()) way of dispatching. If such default behavior of the COMMS library is not good enough for a particular application, it is recommended to use comms::processAllWithDispatchViaDispatcher() function instead, which uses comms::MsgDispatcher to force a particular way of dispatching.

std::size_t consumed =
comms::processAllWithDispatchViaDispatcher<MyDispatcher>(buf, bufLen, protStack, handler);
An auxiliary class to force a particular way of dispatching message to its handler.
Definition MsgDispatcher.h:63

If the described above processing functions (comms::processAllWithDispatch() and comms::processAllWithDispatchViaDispatcher()) are not good enough for a particular application, there are several auxiliary functions that can be used to implement application specific input data processing loop. Please refer to the documentation of:

There are protocols when number of input messages is very limited (one or two) such as various types of acknowledgment. To support such case read() member function of the ProtocolStack as well as processing function(s) described above support reception of reference to the real message object (instead of MsgPtr pointer).

using MyAckMsg = my_protocol::AckMsg<MyMessage>;
MyAckMsg msg;
auto es = comms::processSingleWithDispatch(buf, bufLen, protStack, msg);
// Unexpected message (not AckMsg) has been received, report or handle error
...
}
@ InvalidMsgId
Used to indicate that received message has unknown id.
comms::ErrorStatus processSingleWithDispatch(TBufIter &bufIter, std::size_t len, TFrame &&frame, TMsg &msg, THandler &handler, TExtraValues... extraValues)
Process input until first message is recognized, its object is created and dispatched to appropriate ...
Definition process.h:119

NOTE, that in such case there is no need for either Polymorphic Read of Payload (Deserialisation) or Polymorphic Dispatch Message for Handling. In fact, no virtual function call is used in the code above.

Message Object Allocation

By default, the message object is dynamically allocated. However, some applications (especially bare-metal ones) may require something different. The COMMS library has comms::option::app::InPlaceAllocation option, which may be passed as third template parameter (TAllocationOptions) to ProtocolStack type definition. It statically allocates (in private data members) storage area, that is capable to store any message object from the std::tuple passed as second parameter to ProtocolStack type definition. The message allocation is performed using "placement new" operator, which means only one message object can be allocated at a time. The smart pointer holding the message (MsgPtr) is still std::unique_ptr, but with custom deleter.

In case the Message Interface class does not define any virtual function (which results in lack of virtual destructor), the COMMS library also creates a custom deleter for the defined smart pointer to the allocated message (see MsgPtr) to allow proper downcast of the held pointer to the appropriate class and invoke the correct destructor.

In such cases (that require usage of custom deleters) please refrain from releasing the held object and/or changing the smart pointer type.

Using Generic Message

In general, if message ID cannot be recognised, then appropriate message object cannot be created and processed, i.e. the reading loop will just discard such input. However, there are applications that serve as some kind of a bridge or a firewall, i.e. need to recognise and process only limited number of input messages, all the rest need to be forwarded "as-is" or maybe wrapped in different transport frame before sending it over different I/O link. To help with such task COMMS library provides comms::GenericMessage class. There is also comms::option::app::SupportGenericMessage option that can be passed as third template parameter (TAllocationOptions) to the ProtocolStack type definition. It will force of creation comms::GenericMessage object when appropriate message object is not found.

// Define generic message type
using MyGenericMessage = comms::GenericMessage<MyMessage>;
// Limited number of supported messages
using MyInputMessages =
std::tuple<
my_protocol::Message1<MyMessage>,
my_protocol::Message2<MyMessage>
>;
// Protocol stack definition
using ProtStack =
my_protocol::ProtocolStack<
MyMessage,
MyInputMessages,
>;
Generic Message.
Definition GenericMessage.h:76
Option used to allow comms::GenericMessage generation inside comms::MsgFactory and/or comms::protocol...
Definition options.h:1330

NOTE, that comms::GenericMessage has only single field, which is a list of raw data (comms::field::ArrayList). The read operation of such field will result in copying the data from input buffer to internal storage of this field. The comms::GenericMessage class has also other template parameters (except common message interface class). The second template parameter is option(s) that are going to be passed to this comms::field::ArrayList field. If allocated message is not going to outlive input buffer, than it may make sense to pass comms::option::app::OrigDataView as second template parameter to comms::GenericMessage.

Also note, that it is possible to combine usage of comms::option::app::InPlaceAllocation and comms::option::app::SupportGenericMessage as the third template parameter to ProtocolStack type definition using std::tuple bundling.

using ProtStack =
my_protocol::ProtocolStack<
MyMessage,
MyInputMessages,
std::tuple<
>
>;
Option that forces "in place" allocation with placement "new" for initialisation, instead of usage of...
Definition options.h:1323

Writing Transport Framing and Message Payload

The easiest way to implement write functionality is to use the ability of the message object to perform polymorphic write.

ProtStack protStack; // Protocol stack defined in one of previous sections
void sendMessage(const MyMessage& msg, std::uint8_t* buf, std::size_t len)
{
auto writeIter = comms::writeIteratorFor<MyMessage>(&buf[0]);
auto es = protStack.write(msg, writeIter, len);
... // Send contents of dataToSend via I/O link
}
}

In order to support such polymorphic write the common message interface class MyMessage has to have the following polymorphic functionality

In case the transport framing reports size that follows, the support for Polymorphic Serialisation Length Retrieval is also recommended. It will be used to write the required value right away. However, if not provided the dummy value (0) will be written at first, then after the write operation is complete, the number of written bytes will be calculated and the previously written dummy value updated accordingly. NOTE, that such update is possible only if iterator used for writing is random-access one. Otherwise, such update won't be possible. In this case comms::ErrorStatus::UpdateRequired error status will be returned. It means that the write operation is incomplete, there is a need to perform update() call with random-access iterator. For example, let's assume the Polymorphic Serialisation Length Retrieval is not supported and std::back_insert_iterator<std::vector<std::uint8> > is passed to MyMessage with comms::option::app::WriteIterator option. the message object to perform polymorphic write

ProtStack protStack; // Protocol stack defined in one of previous sections
void sendMessage(const MyMessage& msg, std::vector<std::uint8_t>& outBuf)
{
assert(outBuf.empty()) // Make sure buffer is empty
auto writeIter = std::back_inserter(outBuf);
auto es = protStack.write(msg, writeIter, outBuf.max_size());
if (es == comms::ErrorStatus::UpdateRequired) {
auto updateIter = &outBuf[0];
es = protStack.update(updateIter, outBuf.size());
}
... // Send contents of dataToSend via I/O link
}
}
Main namespace for all classes / functions of COMMS library.

Similar scenario of a need to handle comms::ErrorStatus::UpdateRequired error status may occur when transport framing contains checksum value and output (not random-access) iterator is used. The checksum calculation requires going over the written data to calculate the value. However, it won't be possible to do right away, the update() call must follow.

There may be cases when update() operation also requires knowledge about message object being written. For example when remaining size information shares the same bytes with some extra flags, which can be retrieved from the message object being written. To support such cases there is also overloaded update() member function which also receives reference to message object/

auto updateIter = &outBuf[0];
es = protStack.update(msg, updateIter, outBuf.size());
}

The ProtocolStack does not require usage of polymorphic write for message serialisation all the time. If number of messages being sent is not very high, sometimes it makes sense to avoid adding an ability to support polymorphic write in the common interface. In this case the sending functionality can be implemented using a template function where actual message objects are passed as the parameter:

template <typename TMsg>
void sendMessage(const TMsg& msg)
{
...
auto es = protStack.write(msg, ...);
...
}

Such implementation does not require any polymorphic behaviour from the message object being sent, it takes all the required information from the direct calls to non-virtual doGetId() (see comms::MessageBase::doGetId()) and doLength() (see comms::MessageBase::doLength()). The payload write is also performed using direct call to doWrite() (see comms::MessageBase::doWrite()).

Access to Processed Stack Fields

All the examples above do not store the read/written transport fields anywhere. In most cases it is not needed. However, if need arises they can be cached during the read/write operations and accessed later. The ProtocolStack also defines AllFields type which is std::tuple of all the fields used by all the layer classes.
Also, the ProtocolStack defines readFieldsCached() and writeFieldsCached() member functions which are substitutes to normal read() and write(). The first parameter to these functions is reference to the AllFields bundle object.

ProtStack::AllFields fields;
auto es = protStack.readFieldsCached(fields, msgPtr, readIter, bufSize);

The layer class that is responsible to read/write payload data (see comms::protocol::MsgDataLayer) uses comms::field::ArrayList to define a field that will store the payload when "caching" operations are performed. That's where the fourth template parameter (TPayloadOptions) to ProtocolStack definition comes in play. In case the the input / output buffer outlives the AllFields object, consider passing comms::option::app::OrigDataView option as the fourth template parameter to ProtocolStack definition, which will pass it to to the field containing the message payload raw data. Otherwise, the payload part from the read / written buffer will also be copied to storage area of the cached payload field.

As was mentioned earlier, the protocol stack is defined using so called layer classes (defined in comms::protocol namespace) by wrapping one another. Access to the appropriate layer may be obtained using a sequence of calls to nextLayer() member functions (see comms::protocol::ProtocolLayerBase::nextLayer()). Alternatively, the protocol stack definition is also expected to use COMMS_PROTOCOL_LAYERS_ACCESS() macro to generate a convenience access functions. For example

namespace my_protocol
{
template <
typename TMessage, // common interface class defined by the application
typename TInputMessages = AllMessages<TMessage>, // Input messages that need to be recognised
typename TAllocationOptions = comms::option::app::EmptyOption, // Extra options for MsgIdLayer
typename TPayloadOptions = comms::option::app::EmptyOption // Extra options for payload storage
>
struct ProtocolStack : public
MySyncPrefix<TMessage, TInputMessages, TAllocationOptions, TPayloadOptions>
{
COMMS_PROTOCOL_LAYERS_ACCESS(payload, id, size, checksum, sync);
};
} // namespace my_protocol

The provided names are used in generation of access member functions with layer_ prefix. The code above is equivalent to having the following member functions defined.

template <
typename TMessage, // common interface class defined by the application
typename TInputMessages = AllMessages<TMessage>, // Input messages that need to be recognised
typename TAllocationOptions = comms::option::app::EmptyOption, // Extra options for MsgIdLayer
typename TPayloadOptions = comms::option::app::EmptyOption // Extra options for payload storage
>
struct ProtocolStack : public
MySyncPrefix<TMessage, TInputMessages, TAllocationOptions, TPayloadOptions>
{
// Access to PAYLOAD layer
decltype(auto) layer_payload();
// Const access to PAYLOAD layer
decltype(auto) layer_payload() const;
// Access to ID layer
decltype(auto) layer_id();
// Const access to ID layer
decltype(auto) layer_id() const;
// Access to SIZE layer
decltype(auto) layer_size();
// Const access to SIZE layer
decltype(auto) layer_size() const;
// Access to CHECKSUM layer
decltype(auto) layer_checksum();
// Const access to CHECKSUM layer
decltype(auto) layer_checksum() const;
// Access to SYNC layer
decltype(auto) layer_sync();
// Const access to SYNC layer
decltype(auto) layer_sync() const;
};

Then the access to the appropriate layer as as simple as calling appropriate layer_*() member function. Once the access is obtained, it is possible to call accessCachedField() (see comms::protocol::ProtocolLayerBase::accessCachedField()) member function to get an access to appropriate field. For example:

ProtStack protStack; // Protocol stack object
ProtStack::AllFields fields; // Transport fields
auto es = protStack.readFieldsCached(fields, msgPtr, readIter, bufSize);
... // handle error
return;
}
std::cout << "SYNC = " << protStack.layer_sync().accessCachedField(fields).value() << '\n';
std::cout << "SIZE = " << protStack.layer_size().accessCachedField(fields).value() << '\n';
std::cout << "ID = " << protStack.layer_id().accessCachedField(fields).value() << '\n';
std::cout << "CHECKSUM = " << protStack.layer_checksum().accessCachedField(fields).value() << std::endl;

Version v2.0 the COMMS library has introduced additional way to retrieve values from stripped message framing. There are several functions listed below that can be used to pass extra variadic arguments to the "read" functions of the protocol stack (see read() and readFieldsCached()).

For example there can be a case when Defining Message Interface Class does not support Polymorphic Dispatch Message for Handling. In this case it might be required to retrieve the information about message ID and its relevant index in order to dispatch message object to its appropriate handling function (see Advanced Guide to Message Dispatching).

// Alias to my_protocol::ProtocolStack
using ProtStack = my_protocol::ProtocolStack<MyMessage>
ProtStack protStack; // Protocol stack object
MyHandler handler; // Handler object
ProtStack::MsgPtr msg; Pointer to message to be updated
my_protocol::MsgId msgId = my_protocol::MsgId(); // Message ID to be updated
std::size_t msgIndex = 0U; // Message index to be updated
auto es =
protStack.read(
msg,
readIter,
bufLen,
comms::protocol::msgId(msgId), // msgId will be updated here
comms::protocol::msgIndex(msgIndex)); // msgIndex will be updated here
assert(msg); // Message object is expected to be valid
// Dispatch message knowing its id and index.
comms::dispatchMsg<MyInputMessages>(msgId, msgIndex, *msg, handler);
}
details::MsgIdRetriever< TId > msgId(TId &val)
Add "message ID" output parameter to protocol stack's (frame's) "read" operation.
Definition ProtocolLayerBase.h:1588
details::MsgIndexRetriever msgIndex(std::size_t &val)
Add "message index" output parameter to protocol stack's (frame's) "read" operation.
Definition ProtocolLayerBase.h:1626

Message Handling

When a message is received over I/O link and successfully deserialised, it needs to be dispatched to appropriate handling function. Many developers write quite big (depends on number of messages it needs to handle) switch statement that checks the ID of the message and calls appropriate function in the relevant case area. Other developers try to minimise amount of boilerplate code by introducing hand written map from message ID to the handling function. Such approaches may be quite inefficient in terms of execution performance as well as development effort required to introduce a new message when protocol evolves.

The COMMS library has a built-in efficient (O(1)) dispatch mechanism, which uses "Double Dispatch" idiom. The Polymorphic Dispatch Message for Handling section above described using comms::option::app::Handler option, which adds polymorphic dispatch() member function to the common interface class (MyMessage). The provided handling class (MyHandler) is expected to define handle() member function for every message class it is expected to handle.

class MyHandler
{
public:
void handle(my_protocol::Message1<MyMessage>& msg) {...}
void handle(my_protocol::Message5<MyMessage>& msg) {...}
...
}

and a single handle() member function that receives common message interface class for all the messages that need to be ignored or handled in some other common way. If this function is not added, the compilation may fail.

class MyHandler
{
public:
...
void handle(MyMessage& msg) {} // do nothing for all other messages
}

For example, let's assume that MyHandler defines message to properly handle Message1, but doesn't care about Message2. In this case

std::unique_ptr<MyMessage> msg1(new my_protocol::Message1<MyMessage>);
std::unique_ptr<MyMessage> msg2(new my_protocol::Message2<MyMessage>);
MyHandler handler;
msg1->dispatch(handler); // invokes handle(my_protocol::Message1<MyMessage>&)
msg2->dispatch(handler); // invokes handle(MyMessage&)

NOTE, that MyMessage class is only forward declared when Defining Message Interface Class (MyMessage), but needs to be properly defined (included if in separate file) when defining Transport Framing.

Having Multiple Handlers

There may be a need for being able to dispatch message for multiple independent handlers. Good example would be having a state-machine, where every state processes the same message in different way. It is simple to implement by just defining the handling functions as virtual.

class MyHandler
{
public:
virtual void handle(my_protocol::Message1<MyMessage>& msg) = 0;
virtual void handle(my_protocol::Message2<MyMessage>& msg) = 0;
virtual void handle(my_protocol::Message3<MyMessage>& msg) = 0;
...
};

Defining the concrete handlers:

class Handler1 : public MyHandler
{
public:
virtual void handle(my_protocol::Message1<MyMessage>& msg) override {...};
virtual void handle(my_protocol::Message2<MyMessage>& msg) override {...};
virtual void handle(my_protocol::Message3<MyMessage>& msg) override {...};
...
};
class Handler2 : public MyHandler
{
public:
virtual void handle(my_protocol::Message1<MyMessage>& msg) override {...};
virtual void handle(my_protocol::Message2<MyMessage>& msg) override {...};
virtual void handle(my_protocol::Message3<MyMessage>& msg) override {...};
...
};

Now, any of the handlers may be used to handle the message:

std::unique_ptr<MyMessage> msg(.../* some message object */);
Handler1 handler1;
Handler2 handler2;
msg->dispatch(handler1); // Handle with handler1
msg->dispatch(handler2); // Handle with handler2

Generic Handler

The COMMS library provides some help in defining custom message handlers. There is comms::GenericHandler class that receives at least two template parameters. The first one is a common interface class for all the handled messages (MyMessage). The second template parameter is all the types of all the custom messages the handler is supposed to handle, bundled into std::tuple. Remember defining a bundle of messages (MyInputMessages) that need to be recognised during Reading Transport Framing and Message Payload? It's probably a good place to reuse it or define a new tuple if there is a mismatch.

using MyMessagesToHandle = MyInputMessages;

As the result the comms::GenericHandler implements virtual handle() function for all the provided messages including the provided interface one. The code that automatically generated by comms::GenericMessage is equivalent to the one below.

template<>
class GenericHandler<MyMessage, MyMessagesToHandle>
{
public:
virtual void handle(MyMessage& msg) {} // Do nothing
virtual void handle(my_protocol::Message1<MyMessage>& msg)
{
this->handle(static_cast<MyMessage&>(msg)); // invoke default handling
}
virtual void handle(my_protocol::Message2<MyMessage>& msg)
{
this->handle(static_cast<MyMessage&>(msg)); // invoke default handling
}
...
};

Now, what remains is to inherit from comms::GenericHandler and override the functions that need to be overridden:

class MyHandler : public comms::GenericHandler<MyMessage, AllMessages>
{
public:
// Enable polymorphic delete
virtual ~MyHandler() {}
// Overriding the default handling function
virtual handle(MyMessage& msg) override
{
std::cout << "Ignoring message with ID: " << msg.getId() << std::endl;
}
// Overriding handling of Message1
virtual handle(my_protocol::Message1<MyMessage>& msg) override
{
...; // Handle Message1
}
};
Generic common message handler.
Definition GenericHandler.h:50

Pay attention that comms::GenericHandler doesn't declare its destructor as virtual. If the handler object requires support for polymorphic delete (destruction), make sure to declare its destructor as virtual.

Returning Handling Result

All the examples above used void as a return type from handling functions. It is possible, however, to return value of handling result. In order to achieve this the handler class needs to define inner RetType type and all the handle() functions must return it. For example:

class MyHandler
{
public:
// Return type of all the handle() functions
typedef bool RetType;
bool handle(my_protocol::Message1<MyMessage>& msg) {...}
bool handle(my_protocol::Message2<MyMessage>& msg) {...}
bool handle(my_protocol::Message3<MyMessage>& msg) {...}
...
};

If inner RetType type is defined, it is propagated to be also the return type of the comms::Message::dispatch() member function as well. As the result the developer may use constructs like this:

bool result = msg->dispatch(handler);

If comms::GenericHandler class is used to define the handler class, its third template parameter (which defaults to void), can be used to specify the return type of handling functions.

NOTE, since version v1.1 the COMMS library supports other ways to dispatch a message object to its handling function in addition to one described above. Please read Advanced Guide to Message Dispatching page for more details.

Extra Transport Values

Some protocols may use extra values in their transport information, which may influence the way how message payload is being read and/or message object being handled. Good example would be having a protocol version, which defines what message payload fields were serialised and which were not (because they were introduced in later version of the protocol). Such extra information is stored in the message object itself. If this is the case, the protocol definition is expected to use comms::option::def::ExtraTransportFields option in addition to specifying serialisation endian and message ID type (described in Defining Message Interface Class). (see Fields Definition Tutorial) and bundled in std::tuple:

namespace my_protocol
{
// Field describing protocol version.
using MyVersionField =
MyFieldBase,
std::uint16_t,
comms::option::def::DefaultNumValue<5> // Implementing v5 of the protocol by default
>;
// Relevant extra transport fields, bundled in std::tuple
using MyExtraTransportFields =
std::tuple<
MyVersionField
>;
template <typename... TOptions>
class Message : public
comms::option::def::BigEndian,
comms::option::def::MsgIdType<MsgId>,
comms::option::def::ExtraTransportFields<MyExtraTransportFields>,
TOptions...
>
{
// (Re)definition of the base class as inner Base type
using Base = comms::Message<...>;
public:
COMMS_MSG_TRANSPORT_FIELDS_NAMES(version)
};
} // namespace my_protocol

Usage of comms::option::def::ExtraTransportFields option as well as COMMS_MSG_TRANSPORT_FIELDS_NAMES() macro in the message class definition is equivalent to having the following types and member functions defined

namespace my_protocol
{
template <typename... TOptions>
class Message : public
comms::option::def::BigEndian,
comms::option::def::MsgIdType<MsgId>,
TOptions...
>
{
public:
// Type of extra fields
using TransportFields = MyExtraTransportFields;
// Accessors for defined transport fields
TransportFields& transportFields() { return m_transportFields; }
const TransportFields& transportFields() const { return m_transportFields; }
// Indices to access extra transport fields
enum TransportFieldIdx
{
TransportFieldIdx_version,
TransportFieldIdx_numOfValues
};
// Access the "version" extra transport field
auto transportField_version() -> decltype(std::get<TransportFieldIdx_version>(transportFields()))
{
return std::get<TransportFieldIdx_version>(transportFields());
}
// Access the "version" extra transport field (const version)
auto transportField_version() const -> decltype(std::get<TransportFieldIdx_version>(transportFields()))
{
return std::get<TransportFieldIdx_version>(transportFields());
}
// Definition of the transport field's types
using TransportField_version = MyVersionField;
private:
TransportFields m_transportFields;
};
} // namespace my_protocol

For reference see also description of comms::Message::transportFields() member function.

Access to the version information given a reference to message object may now be implemented as:

void handle(my_protocol::Message<>& msg)
{
// Retrieve the version numeric value
auto versionValue = msg.transportField_version().value();
... // do something with version information
}

NOTE, that the defined "extra transport fields" are there to attach some extra information, delivered as part of transport framing, to message object itself. These fields are NOT getting serialised / deserialised when message object (payload) being read / written.

The comms::Message interface class defines comms::Message::hasTransportFields() static constexpr member function, which may be used at compile time to determine whether the comms::option::def::ExtraTransportFields option has been used, i.e. the message interface class defines mentioned earlier types and functions.

Built-in Version Support

The COMMS library contain a built-in protocol version support when version info is provided within Extra Transport Values. To support this feature, the message interface definition class needs to use comms::option::def::VersionInExtraTransportFields option, in addition to comms::option::def::ExtraTransportFields option itself, to specify which field is version.

namespace my_protocol
{
// Field describing protocol version.
using MyVersionField = comms::field::IntValue<...>;
// Relevant extra transport fields, bundled in std::tuple
using MyExtraTransportFields =
std::tuple<
MyVersionField
>;
template <typename... TOptions>
class Message : public
comms::option::def::BigEndian,
comms::option::def::MsgIdType<MsgId>,
comms::option::def::ExtraTransportFields<MyExtraTransportFields>,
comms::option::def::VersionInExtraTransportFields<0>
TOptions...
>
{
...
};
} // namespace my_protocol

Usage of comms::option::def::VersionInExtraTransportFields option generates inner VersionType type (see comms::Message::VersionType) as well as version() access functions (see comms::Message::version()) for direct access to it. It is equivalent to having the following functions defined:

namespace my_protocol
{
template <typename... TOptions>
class Message : public
{
public:
...
// Type of extra fields
using VersionType = ...; // Equals to ValueType of the relevant field.
// Accessors for the version info
VersionType& version();
const VersionType& version() const;
};
} // namespace my_protocol

NOTE, that updating the version information only modifies the value of the relevant transport fields itself. The message contents are not being updated. There is a need to invoke doFieldsVersionUpdate() member function (see comms::MessageBase::doFieldsVersionUpdate()), which will do the job of updating message contents accordingly.

using MyMessage1 = my_protocol::MyMessage<...>;
MyMessage1 msg;
msg.version() = 5U; // Update the version
msg.doFieldsVersionUpdate(); // Update the message contents that depend on version

The refresh functionality (direct or polymorphic) also contains update of the fields' version, which can be used instead.

void updateVersion(MyMessage& msg)
{
msg.version() = 5U;
msg.refresh(); // Polymorphically update the message contents accordingly
}

Pseudo Transport Values

Some communication protocols have values (such as protocol version information), which are reported in the payload of one of the messages and may influence the way how other messages being deserialised and/or handled. Usually it is some kind of CONNECT message. Such scenario is implemented in the very similar way to Extra Transport Values. The protocol stack is still defined using comms::protocol::TransportValueLayer but with comms::option::def::PseudoValue option. Such layer contains the "pseudo" field in its internal data members and pretends to read it during read operation.

Let's assume the protocol framing is defined to be

SIZE | ID | PAYLOAD

but there is some kind of CONNECT message that reports client protocol version. The transport framing will probably be defined as if it handles the following stack.

SIZE | ID | VERSION (pseudo) | PAYLOAD

The transport framing is expected to be defined as below providing an access to the VERSION handling layer:

namespace my_protocol
{
template <
typename TMessage, // common interface class defined by the application
typename TInputMessages = AllMessages<TMessage>, // Input messages that need to be recognised
typename TAllocationOptions = comms::option::app::EmptyOption, // Extra options for MsgIdLayer
typename TPayloadOptions = comms::option::app::EmptyOption // Extra options for payload storage
>
struct ProtocolStack : public
MySizePrefix<TMessage, TInputMessages, TAllocationOptions, TPayloadOptions>
{
COMMS_PROTOCOL_LAYERS_ACCESS(payload, version, id, size);
};
} // namespace my_protocol

Then after reception and handling of the mentioned CONNECT message, the stored pseudo version field may be accessed using pseudoField() member function of comms::protocol::TransportValueLayer layer (see comms::protocol::TransportValueLayer::pseudoField()) and updated with reported value.

ProtStack protStack; // Protocol stack object
protStack.layer_version().pseudoField().value() = connectMsg.field_version().value();

From now on every received message will have appropriate extra transport field updated accordingly right after receiving of the message and before its read operation being performed.

Application Specific Customisation of Messages

When most of the messages are uni-directional, i.e. are either only sent or only received, then it may make sense to split the common message interface class MyMessage (see Defining Message Interface Class) into two interface classes: for input and for output. It will save the generation of unnecessary virtual functions which are not used, but occupy space.

using MyInputMessage =
my_protocol::Message<
>;
using MyOutputMessage =
my_protocol::Message<
comms::option::app::IdInfoInterface, // polymorphic ID retrieve
comms::option::app::LengthInfoInterface // polymorphic serialisation length retrieve
>;
using MyInputMessage1 = my_protocol::Message1<MyInputMessage>;
using MyInputMessage2 = my_protocol::Message2<MyInputMessage>;
using MyInputMessage3 = my_protocol::Message3<MyInputMessage>;
...
using MyOutputMessage7 = my_protocol::Message7<MyOutputMessage>;
using MyOutputMessage8 = my_protocol::Message8<MyOutputMessage>;
using MyOutputMessage9 = my_protocol::Message9<MyOutputMessage>;
...

There may be also an opposite case, when most of the messages are bi-directional, while only few go one way. For most applications the single common message interface will do the job. However generating unnecessary virtual functions for bare-metal application may be a heavy price to pay, especially when ROM size is small. The COMMS library provides several options that inhibit generation of virtual functions. These extra options need to be passed to comms::MessageBase class when defining a message class. Available options are:

In order to be able to pass these extra options to message definition classes, the support from the latter is required. If the protocol definition follows a recommended approach the message definition is expected to reuse extra protocol options structure (DefaultOptions) described earlier.

namespace my_protocol
{
// Allow passing extra options to messages as well
struct DefaultOptions
{
struct message
{
...
};
};
namespace message
{
template <typename TBase, typename TOpt = DefaultOptions>
class Message1 : public
TBase,
comms::option::def::StaticNumIdImpl<MsgId_Message1>,
comms::option::def::FieldsImpl<Message1Fields<TOpt>::All>,
comms::option::def::MsgType<Message1<TBase, TOpt> >,
typename TOpt::message::Message1 // Extra options
>
{
...
};
} // namespace message
} // namespace my_protocol

As an example let's assume, that Message1 is only sent by the application, but never received, and Message 2 is the opposite, only received by never sent. In this case defining the following options structure and passing them in to message definition classes will work

struct MyOptions : public my_protocol::DefaultOptions
{
struct message : public my_protocol::DefaultOptions::message
{
};
};
using MyMessage1 = my_protocol::Message1<MyMessage, MyOptions>;
using MyMessage2 = my_protocol::Message2<MyMessage, MyOptions>;
Option that inhibits implementation of comms::MessageBase::readImpl() regardless of other availabilit...
Definition options.h:1298
Option that inhibits implementation of comms::MessageBase::writeImpl() regardless of other availabili...
Definition options.h:1303

Message Interface Extension

Sometimes the public interface of the messages, generated by the COMMS library out of available options passed to comms::Message, may be insufficient for some applications and its interface needs to be extended with custom member functions. It is easy to achieve by just implementing required function in common message interface class

class MyMessage : public my_protocol::Message<...>
{
public:
void someFunc()
{
someFuncImpl();
}
protected:
virtual void someFuncImpl() = 0;
};

All the protocol messages must also be extended in order to implement missing virtual function

class MyMessage1 : public my_protocol::Message1<MyMessage>
{
protected:
virtual void someFuncImpl() override
{
...
}
};

Don't forget to bundle newly defined messages in std::tuple and pass them to Transport Framing definition or Generic Handler when needed.

using MyInputMessages =
std::tuple<
MyMessage1,
MyMessage2,
...
>;
using MyProtocolStack = my_protocol::ProtocolStack<MyMessage, MyInputMessages>;
...