COMMS
Template library intended to help with implementation of communication protocols.
How to Define New Custom Protocol

The protocol definition is mostly about defining messages and their fields. As the first stage please read Fields Definition Tutorial in order to understand what types of fields are available and how to define them.

Headers and Libraries

COMMS is a headers only library without any object files to link against. In order to include the whole functionality of the library please use single include statement:

#include "comms/comms.h"
Aggregates all the includes of the COMMS library interface.

If the protocol grows and the compilation takes a significant amount of time, more fine-grained include statements may be used:

#include "comms/fields.h" // Provides all definitions from comms::field namespace
#include "comms/protocols.h" // Provides all definitions from comms::protocol namespace
#include "comms/units.h" // Provides all definitions from comms::units namespace
#include "comms/Message.h" // Definition of comms::Message class to define interface
#include "comms/MessageBase.h" // Definition of comms::MessageBase class to define message impl
#include "comms/GenericHandler.h" // Definition of comms::GenericHandler class
#include "comms/MsgFactory.h" // Definition of comms::MessageFactory class
This file contains definition of common handler.
Provides common base class for the custom messages with default implementation.
Contains definition of Message object interface and various base classes for custom messages.
Contains definition of comms::MsgFactory class.
This file provides all the definitions from comms::field namespace.
This file provides all the definitions from comms::protocol namespace.
This file contains all the functions required for proper units conversion.

Checking pre- and post- Conditions

The COMMS library is intended to be used in embedded systems (including bare metal), which may have standard library excluded from the compilation. In order to check pre- and post- conditions as well as inner assumptions, please use COMMS_ASSERT macro (instead of standard assert()). It gives and ability to the application being developed in the future to choose and use its own means to report assertion failures.

Configuration Options

Many classes provided by the COMMS library allow change and/or extention of their default functionality using various options. The option classes and/or types that can (and should) be used to define a protocol reside in comms::option::def namespace. All option classes / types that intended to be used by the end application for its own customization reside in comms::option::app namespace and should NOT be used in protocol definition.

Common Interface Class

Protocol definition needs to be started by defining all the available numeric IDs of the messages as separate enum. For example in file MsgId.h

// file MsgId.h
#pragma once
#include <cstdint>
namespace my_protocol
{
enum MsgId : std::uint8_t
{
MsgId_Message1,
MsgId_Message2,
MsgId_Message3,
...
};
} // namespace my_protocol

NOTE, that most probably the same enum will be used to define a field that responsible to read / write message ID information in the transport framing. That's the reason why the underlying type of the enum needs to be specified (please see Enum Value Fields for more details).

After the numeric IDs are specified, there is a need to define common message interface class by extending comms::Message. The defined class / type needs to pin the type used for message IDs with comms::option::def::MsgIdType and defined earlier enum. It also should specify the serialisation endian with either comms::option::def::BigEndian or comms::option::def::LittleEndian options. However it must also allow extension with other options by the application.

// file Message.h
#pragma once
namespace my_protocol
{
template <typename... TOptions>
using Message =
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

The application, that is going to use protocol definition later, will use extra options to specify polymorphic behaviour it needs.

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). Another example is having some kind of flags relevant to all the messages. Such extra information needs to be stored in the message object itself. The most straightforward way of achieving this is to define appropriate API functions and member fields in common message interface class:

// file Message.h
#pragma once
namespace my_protocol
{
template <typename... TOptions>
class MyMessage : public
comms::option::def::BigEndian,
comms::option::def::MsgIdType<MsgId>,
TOptions...
>
{
public:
unsigned getVersion() const { return m_version; }
void setVersion(unsigned value) { m_version = value; }
std::uint8_t getFlags() const { return m_flags; }
void setFlags(std::uint8_t value) { m_flags = value; }
private:
unsigned m_version = 0;
std::uint8_t m_flags = 0;
};
} // namespace my_protocol

HOWEVER, it will require implementation of a custom protocol transport layer (See Implementing New Layers) that read the required values and re-assign them to the message object using appropriate API function. The COMMS library has a built-in way to automate such assignments (see Extra Transport Values section in Protocol Stack Definition Tutorial). In order to support usage of comms::protocol::TransportValueLayer the message interface class must define "extra transport fields".

First, such extra transport field(s) must be defined using a field abstraction (see Fields Definition Tutorial) and bundled in std::tuple:

namespace my_protocol
{
// Base class of all the fields
// Field describing protocol version.
// Field describing protocol version.
// Relevant extra transport fields, bundled in std::tuple
using MyExtraTransportFields =
std::tuple<
MyVersionField,
MyFlagsField
>;
} // namespace my_protocol
Base class to all the field classes.
Definition: Field.h:33
Bitmask value field.
Definition: BitmaskValue.h:103
Field that represent integral value.
Definition: IntValue.h:72

Second, provide the defined tuple to message interface class using comms::option::def::ExtraTransportFields option.

namespace my_protocol
{
template <typename... TOptions>
class Message : public
comms::option::def::BigEndian,
comms::option::def::MsgIdType<MsgId>,
comms::option::def::ExtraTransportFields<MyExtraTransportFields>,
TOptions...
>
{
};
} // namespace my_protocol

It is equivalent to having the following public interface defined:

namespace my_protocol
{
template <typename... TOptions>
class MyMessage : 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; }
private:
TransportFields m_transportFields;
};
} // namespace my_protocol

An access to the version and flags information, given a reference to the message object will look like this:

void handle(my_protocol::Message<>& msg)
{
// Access to tuple of extra transport fields
auto& extraTransportFields = msg.transportFields();
// Access the version field
auto& versionField = std::get<0>(extraTransportFields);
// Retrieve the version numeric value
auto versionValue = versionField.value();
// Access the flags field
auto& flagsField = std::get<1>(extraTransportFields);
... // do something with version and flags information
}

The version and/or flags information may be accessed in the same way when implementing Custom Read Functionality and acted accordingly.

NOTE, that example above accesses the index of the field within the holding tuple. For convenience, the COMMS library defines COMMS_MSG_TRANSPORT_FIELDS_NAMES() macro which allows providing meaningful names to the extra transport fields:

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 is
// required by COMMS_MSG_TRANSPORT_FIELDS_NAMES() macro
using Base = comms::Message<...>;
public:
};
#define COMMS_MSG_TRANSPORT_FIELDS_NAMES(...)
Provide names for extra transport fields.
Definition: Message.h:716
constexpr unsigned version()
Version of the COMMS library as single numeric value.
Definition: version.h:64

NOTE requirement to (re)define base class as inner Base type. It is needed to be able to access TransportFields type defined by the comms::Message.

Using such macro is equivalent to manually defining the following public interface:

template <typename... TOptions>
class Message : public
comms::option::def::BigEndian,
comms::option::def::MsgIdType<MsgId>,
comms::option::def::ExtraTransportFields<MyExtraTransportFields>,
TOptions...
>
{
public:
enum TransportFieldIdx
{
TransportFieldIdx_version,
TransportFieldIdx_flags,
TransportFieldIdx_numOfValues
};
auto transportField_version() -> decltype(std::get<TransportFieldIdx_version>(transportFields()))
{
return std::get<TransportFieldIdx_version>(transportFields());
}
auto transportField_version() const -> decltype(std::get<TransportFieldIdx_version>(transportFields()))
{
return std::get<TransportFieldIdx_version>(transportFields());
}
auto transportField_flags() -> decltype(std::get<TransportFieldIdx_flags>(transportFields()))
{
return std::get<TransportFieldIdx_flags>(transportFields());
}
auto transportField_flags() const -> decltype(std::get<TransportFieldIdx_flags>(transportFields()))
{
return std::get<TransportFieldIdx_flags>(transportFields());
}
// Definition of the transport field's types
using TransportField_version = ...;
using TransportField_flags = ...;
};
TransportFields & transportFields()
Get access to extra transport fields.

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 being read / written.

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

template <typename... TOptions>
class Message : public
comms::option::def::BigEndian,
comms::option::def::MsgIdType<MsgId>,
comms::option::def::ExtraTransportFields<MyExtraTransportFields>,
TOptions...
>
{
public:
};
#define COMMS_MSG_TRANSPORT_FIELDS_ACCESS(...)
Add convenience access enum and functions to extra transport fields.
Definition: Message.h:575

In fact COMMS_MSG_TRANSPORT_FIELDS_NAMES() is implemented as the wrapper around COMMS_MSG_TRANSPORT_FIELDS_ACCESS().

Version in Extra Transport Values

As was described in previous section, the extra transport values may contain protocol version information. The COMMS library contains extra functionality to help with protocol versioning. If extra transport values contain version, then it is recommended to let the library know which field in the provided ones is the version. To do so comms::option::def::VersionInExtraTransportFields, with index of the field as a template argument needs to be used.

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...
>
{
public:
...
};

This option adds the following type and convenience access member functions.

template <typename... TOptions>
class Message : public
{
public:
...
// Type used for version storage
using VersionType = ...;
// Convenience access to the version value
const VersionType& version() const;
};
VersionType & version()
Access to version information.
comms::option::def::VersionType< T > VersionType
Same as comms::option::def::VersionType.
Definition: options.h:1797

If message contents depend on the version of the protocol, this information will be used inside Message Implementation Class (described below) to properly implement various functionalities. More details in Protocol Version Support section below.

Message Implementation Class

The next stage is to define protocol messages with their fields.

Recommended Practice

  • Use separate header file for every message class.
  • Use separate folder and/or namespace for all the messages (for example message)
  • Define all the relevant to the message fields in the same file, but in separate scope (struct), that has the same name as the message, but has extra suffix (*Fields).
  • Define inner type (for example All) that bundles all the defined fields in single std::tuple.

For example, let's assume there is a protocol message called "Message1". Then, define it in separate header filed (Message1.h)

#pragma once
#include "comms/comms.h"
#include "MyFieldBase.h" // Defines MyFieldBase common base class for all the fields
namespace my_protocol
{
namespace message
{
struct Message1Fields
{
using field2 = ...
using field3 = ...
// bunding type
using All =
std::tuple<
field1,
field2,
field3
>;
};
// Definition of the message itself, described and explained later
template <typename TBase>
class Message1 : public comms::MessageBase<TBase, ...>
{
...
};
} // namespace message
} // namespace my_protocol
Base class for all the custom protocol messages.
Definition: MessageBase.h:83

The message definition class has to extend comms::MessageBase and receive at least one template parameter, that is passed as first one to comms::MessageBase.

template <typename TBase>
class Message1 : comms::MessageBase<TBase, ...>
{
...
};

The TBase template parameter is chosen by the application being developed. It is expected to be a variant of Common Interface Class (my_protocol::Message), which specifies polymorphic interface that needs to be implemented. The comms::MessageBase class in turn will publicly inherit from the provided common interface class. As the result the full class inheritance graph may look like this:


There are at least 3 additional options that should be passed to comms::MessageBase.

For example

...
template <typename TBase>
class Message1 : public
TBase,
comms::option::def::StaticNumIdImpl<MsgId_Message1>,
comms::option::def::FieldsImpl<Message1Fields::All>,
comms::option::def::MsgType<Message1<TBase> > // type of the message being defined
{
...
};
} // namespace message
} // namespace my_protocol

It is equivalent to having the following types and NON-virtual functions defined

template <typename TBase>
class Message1 : public TBase
{
public:
// Redefining the provided std::tuple of fields as internal type
using AllFields = ... /* std::tuple of fields */;
// Access the stored std::tuple of fields
AllFields& fields()
{
return fields_;
}
// Access the stored std::tuple of fields
const AllFields& fields() const
{
return fields_;
}
// Default implementation of read functionality
template <typename TIter>
comms::ErrorStatus doRead(TIter& iter, std::size_t len)
{
// Read all the fields one by one by invoking
// read() member function of all the fields.
}
// Default implementation of write functionality
template <typename TIter>
comms::ErrorStatus doWrite(TIter& iter, std::size_t len) const
{
// Write all the fields one by one by invoking
// write() member function of all the fields.
}
// Default implementation of validity check functionality
bool doValid() const
{
// Call to valid() member function of all the fields one by one.
// The message is valid if all the fields are valid.
}
// Default implementation of length calculation functionality
std::size_t doLength() const
{
// Invoke the length() member function of every field and
// report sum of the values
}
// Defalut implementation of the refreshing functionality
bool doRefresh()
{
// Invokes refresh() member function of every field and returns
// true if at least one of the fields has been updated (returned true).
}
private:
AllFields fields_; // Fields stored as tuple.
};
bool doRefresh() const
Default implementation of refreshing functionality.
ErrorStatus doRead(TIter &iter, std::size_t size)
Default implementation of read functionality.
std::size_t doLength() const
Default implementation of length calculation functionality.
ErrorStatus doWrite(TIter &iter, std::size_t size) const
Default implementation of write functionality.
AllFields & fields()
Get an access to the fields of the message.
bool doValid() const
Default implementation of validity check functionality.
ErrorStatus
Error statuses reported by the Communication module.
Definition: ErrorStatus.h:17

See also relevant API documentation:

In case the passed message interface class as template parameter (TBase) defines some polymorphic interface functions, their implementation is automatically generated by the comms::MessageBase. For example if passed interface class required polymorphic read operation, the following member function will also be automatically implemented:

template <typename TBase>
class Message1 : public TBase
{
...
protected:
virtual comms::ErrorStatus readImpl(ReadIterator& iter, std::size_t len)
{
return doRead(iter, len);
}
};
virtual ErrorStatus readImpl(ReadIterator &iter, std::size_t size) override
Implementation of polymorphic read functionality.
comms::option::app::ReadIterator< TIter > ReadIterator
Same as comms::option::app::ReadIterator.
Definition: options.h:1835

Providing Names to the Fields

When preparing message object to send or when handling received message, the fields it contains need to be accessed to set or get their values. The default (build-in) way of achieving that is to get access to the fields tuple using inherited comms::MessageBase::fields() member function and then using std::get() function to access the fields inside the tuple.

Message1<SomeInterface> msg;
auto& allFields = msg.fields();
auto& field1 = std::get<0>(allFields);
auto& field2 = std::get<1>(allFields);
field1.value() = 100;
field2.value() = 32;
sendMessage(msg);

Although it works, it is not very convenient way to access and operate the fields. There is COMMS_MSG_FIELDS_NAMES() macro that allows to provide meaningful names for the fields:

template <typename TMessage>
class Message1 : public
comms::MessageBase<... /* options here */>
{
// Definition of the Base class is a requirement
using Base = comms::MessageBase<... /* options here */>;
public:
// Provide names for the fields
COMMS_MSG_FIELDS_NAMES(value1, value2, value3);
};

NOTE requirement to (re)define base class as inner Base type. It is needed to be able to access AllFields type defined by the comms::MessageBase.

The said macro creates the following definitions of inner enum FieldIdx type, field_* accessor functions as well as Field_* type definitions.

template <typename TMessage>
class Message1 : public
comms::MessageBase<... /* options here */>
{
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());
}
// Definition of the field's types
using Field_value1 = Field1;
using Field_value2 = Field2;
using Field_value3 = Field3;
};

As the result, accessing to the fields becomes much easier and clearer:

Message1<SomeInterface> msg;
msg.field_value1().value() = 100;
msg.field_value2().value() = 32;
sendMessage(msg);

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

template <typename TMessage>
class Message1 : public
comms::MessageBase<... /* options here */>
{
public:
// Provide names for the fields
COMMS_MSG_FIELDS_ACCESS(value1, value2, value3);
};

In fact COMMS_MSG_FIELDS_NAMES() is implemented as the wrapper around COMMS_MSG_FIELDS_ACCESS().

Custom Read Functionality

The default read functionality implemented by comms::MessageBase::doRead() is to invoke read() member function of every field and return success if all the invocations returned success. Sometimes such default implementation may be incomplete or incorrect and may require additional or different implementation. It is very easy to fix by defining new doRead() public member function with updated functionality. The comms::MessageBase class contains inner "magic" to call the provided doRead() instead of default one when implementing virtual comms::MessageBase::readImpl(). As an example let's define new message type (Message2), which has two fields. The first one is a 1 byte bitmask, the least significant bit of which defines whether the second field exists. The second field is optional 2 byte unsigned integer one.

// file Message2.h
#include "comms/comms.h"
#include "MyFieldBase.h" // Defines MyFieldBase common base class for all the fields
namespace my_protocol
{
namespace message
{
class Message2Fields
{
using field1 =
MyFieldBase,
>;
using field2 =
>;
// bundle all the fields
using All = std::tuple<
field1,
field2
>;
};
template <typename TBase>
class Message2 : public
TBase,
comms::option::def::StaticNumIdImpl<MsgId_Message2>,
comms::option::def::FieldsImpl<Message2Fields::All>,
comms::option::def::MsgType<Message2<TBase> >
>
{
// Redefinition of the base class as inner Base type, required
// by COMMS_MSG_FIELDS_NAMES() macro.
using Base =
TBase,
>;
public:
COMMS_MSG_FIELDS_NAMES(flags, data);
template <typename TIter>
comms::ErrorStatus doRead(TIter& iter, std::size_t len)
{
// Get type of the base class
using Base = typename std::decay<decltype(comms::toMessageBase(*this))>::type;
// Read only the flags value
auto es = Base::template doReadUntilAndUpdateLen<FieldIdx_data>(iter, len);
return es;
}
// Update mode (exists/missing) of the optional value to follow
if (field_flags().value() != 0) {
field_data().setExists();
else {
field_data().setMissing();
}
// Read the rest of the fields
return Base::template doReadFrom<FieldIdx_data>(iter, len);
}
};
Adaptor class to any other field, that makes the field optional.
Definition: Optional.h:45
@ Success
Used to indicate successful outcome of the operation.
MessageBase< TMessage, TOptions... > & toMessageBase(MessageBase< TMessage, TOptions... > &msg)
Upcast type of the message object to comms::MessageBase in order to have access to its internal types...
Definition: MessageBase.h:879
Option that specifies custom validation class.
Definition: options.h:662
Option that specifies default initialisation class.
Definition: options.h:616
Option used to specify fields of the message and force implementation of default read,...
Definition: options.h:234
Option used to specify number of bytes that is used for field serialisation.
Definition: options.h:280
Option used to specify actual type of the message.
Definition: options.h:202
Option used to specify numeric ID of the message.
Definition: options.h:193

Please note, that due to the fact that defined message class is a template one, the member functions defined in comms::MessageBase are not accessible directly, there is a need to specify the base class scope. If there is no inner Base type defined in the class scope (required to support clang and earlier versions of gcc), it is possible to use comms::toMessageBase() function to detect it.

Also note, that comms::MessageBase provides the following member functions in order to allow read of the selected fields.

Custom Refresh Functionality

The default refresh functionality implemented by comms::MessageBase::doRefresh() is to invoke refresh() member function of every field. The function will return true (indicating that at message contents have been updated) if at least one of the fields returns true. Let's take a look again at the definition of Message2 mentioned earlier, where the existence of second field (data) depends on the value of the least significant bit in the first field (flags). During read operation the mode of the data is updated after value of the flags is read. However, when preparing the same message for write, there is a chance that message contents are going to be put in invalid state:

Message2<SomeInterface> msg;
msg.field_flags().value() = 0x1;
msg.field_data().field().value() = 10U;
msg.field_data().setMissing(); // Bug, the field should exist
sendMessage(msg);

If message is sent, the flags will indicate that the data field follows, but it won't be serialised, due to being marked as "missing" by mistake. Please note, that all the "write" functions are marked as const and are not allowed to update the message fields during write operation. It may be useful to have member function that brings message contents into the valid and consistent state. It should be called doRefresh() and return boolean value (true in case the message contents were updated, and false if they remain intact.

template <typename TBase>
class Message2 : public comms::MessageBase<... /* options here */>
{
using Base = comms::MessageBase<... /* options here */>;
public:
COMMS_MSG_FIELDS_NAMES(flags, data);
bool doRefresh()
{
auto expectedDataMode = comms::field::OptionalMode::Missing;
if ((field_flags().value() & 0x1) != 0U) {
}
if (field_data().getMode() == expectedDataMode) {
// No need to change anything
return false;
}
field_data().setMode(expectedDataMode);
return true;
}
};
@ Exists
Field must exist.
@ Missing
Field doesn't exist.

As the result the code preparing message for sending may look like this:

Message2<SomeInterface> msg;
msg.field_flags().value() = 0x1;
msg.field_data().field().value() = 10U;
msg.doRefresh(); // Bring message contents into a valid state
sendMessage(msg);

In order to support polymorphic refresh functionality when required (see Keeping Message Contents in a Consistent State), the actual message class implementation must also pass comms::option::def::HasCustomRefresh option to comms::MessageBase class. Failure to do so may result in missing implementation of comms::MessageBase::refreshImpl(). In this case, the default implementation of comms::Message::refreshImpl() will be used instead, always returning false (reporting that message fields weren't updated) without proper execution of refresh functionality.

template <typename TBase>
class Message2 : public
TBase,
comms::option::def::StaticNumIdImpl<MyMsgId_Message2>,
comms::option::def::MsgType<Message2<TMessage> >,
comms::option::def::FieldsImpl<Message2Fields> ,
comms::option::def::HasCustomRefresh // Support polymorphic refresh when needed
>
{
using Base = comms::MessageBase<... /* options here */>;
public:
COMMS_MSG_FIELDS_NAMES(flags, data);
template <typename TIter>
comms::ErrorStatus doRead(TIter& iter, std::size_t len)
{
... // see implementation above
}
bool doRefresh()
{
... // see implementation above
}
};

Custom Write Functionality

Usually there is no need to provide custom write functionality for the messages in consistent state (see Custom Refresh Functionality). The default one implemented by comms::MessageBase::doWrite(), which invokes write() member function of every field, is correct. However, if the need arises it is enough just to provide custom doWrite() member function.

template <typename TBase>
class SomeMessage : public comms::MessageBase<...>
{
using Base = comms::MessageBase<...>;
public:
template <typename TIter>
comms::ErrorStatus doWrite(TIter& iter, std::size_t len) const
{
...
}
};
#define COMMS_MSG_FIELDS_NAMES(...)
Provide names for message fields.
Definition: MessageBase.h:1066

Note, that comms::MessageBase provides the following member functions in order to allow write of the selected fields.

Custom Length Calculation

Just like with Custom Write Functionality providing custom length calculation is usually not required, but if need arises, just provide your own variant of doLength() member function.

template <typename TBase>
class SomeMessage : public comms::MessageBase<...>
{
public:
std::size_t doLength() const
{
...
}
};

Note, that comms::MessageBase provides the following member functions in order to allow length calculation of the selected fields.

Custom Validity Check

The default implementation of comms::MessageBase::doValid() calls valid() member function of every message field and returns true if all the calls returned true. However, there may be a need to provide extra checks in case specific value of one field may require tighter constrains on the value of another. In order to provide custom validity check, just implement custom doValid() member function.

template <typename TBase>
class SomeMessage : public comms::MessageBase<...>
{
public:
bool doValid() const
{
// Get type of the base class
using Base = typename std::decay<decltype(comms::toMessageBase(*this))>::type;
// Check that all fields are valid by themselves
if (!Base::doValid()) {
return false;
}
... // do custom validation logic
return true;
}
};

Reporting Message Name

Some application may require printing (or reporting by other means) human readable name of the message. The comms::MessageBase cannot automatically generate appropriate function. As the result, the message definition class is expected to define doName() member function with the following signature.

template <typename TBase>
class SomeMessage : public comms::MessageBase<...>
{
public:
static const char* doName()
{
return "Some Message";
}
};

In order to support Polymorphic Message Name Retrieval there is a need to pass comms::option::def::HasName option to comms::MessageBase to notify the latter about existence of doName() member function.

template <typename TBase>
class SomeMessage : public
...,
comms::option::def::HasName
>
{
public:
static const char* doName()
{
return "Some Message";
}
};

When comms::option::def::HasName is used, the comms::MessageBase creates overriding nameImpl() (see comms::MessageBase::nameImpl()), which invokes provided doName() member function.

Protocol Version Support

As was described earlier in Version in Extra Transport Values, some protocols may include version information in either message transport framing or in one of the messages used to establish a connection. Every field defines setVersion() member function in its public interface. The comms::MessageBase class will automatically call this function for every field before performing its read operation (inside comms::MessageBase::doRead()). However, if Custom Read Functionality is implemented, the latter is expected to call provided comms::MessageBase::doFieldsVersionUpdate() member function explicitly before attempting actual read operations. NOTE, that the comms::MessageBase::doFieldsVersionUpdate() function exists only if comms::option::def::VersionInExtraTransportFields has been provided to message interface.

template <typename TBase>
class Message2 : public
{
public:
...
template <typename TIter>
comms::ErrorStatus doRead(TIter& iter, std::size_t len)
{
// Get type of the base class
using Base = typename std::decay<decltype(comms::toMessageBase(*this))>::type;
// Update version of the fields
Base::doFieldsVersionUpdate();
// Do the actual read
...
}
};

Similarly, the fields' version needs to be updated in case Custom Refresh Functionality is implemented.

template <typename TBase>
class Message2 : public
{
public:
...
bool doRefresh()
{
// Get type of the base class
using Base = typename std::decay<decltype(comms::toMessageBase(*this))>::type;
// Update version of the fields
bool updated = Base::doFieldsVersionUpdate();
... // Custom refresh functionality
return updated;
}
};

Aliases to Field Names

When the communication protocol evolves and new versions of it are being released, it may happen that some fields get renamed to give them a different or refined meaning. Simple change of the name may result in old client code not being able to compile with new versions of the protocol definition library. To help with such case the COMMS library provides several macros that can generate alias names for renamed fields.

Aliases to Field Names Inside Message Definition

The COMMS library provides COMMS_MSG_FIELD_ALIAS() macro, which is expected to be used after COMMS_MSG_FIELDS_NAMES() one when new message class is being defined. As an example let's change the value3 from the example in the Providing Names to the Fields section to be newValue3.

template <typename TMessage>
class Message1 : public comms::MessageBase<...>
{
using Base = comms::MessageBase<...>;
public:
// Provide names for the fields
COMMS_MSG_FIELDS_NAMES(value1, value2, newValue3);
COMMS_MSG_FIELD_ALIAS(value3, newValue3);
};

The usage of COMMS_MSG_FIELD_ALIAS() above generates the following alias type as well as convenience access functions:

template <typename TMessage>
class Message1 : public comms::MessageBase<...>
{
using Base = comms::MessageBase<...>;
public:
// Provide names for the fields
COMMS_MSG_FIELDS_NAMES(value1, value2, newValue3);
// Alias to the new type
using Field_value3 = Field_newValue3;
// Access to "newValue3" via "value3" name
auto field_value3() -> decltype(field_newValue3())
{
return field_newValue3();
}
// Const access to "newValue3" via "value3" name
auto field_value3() const -> decltype(field_newValue3())
{
return field_newValue3();
}
};

In this case the old client code that tries to access appropriate field using field_value3() access function will still work after renaming takes place.

Another common case is when some field (usually comms::field::IntValue or comms::field::EnumValue) has a limited range of possible values and in order to save on some I/O traffic, the developer decides to split the value storage into multiple small parts and make it a comms::field::Bitfield instead. In order to keep old client code compiling and working the COMMS_MSG_FIELD_ALIAS() may be used:

template <typename TMessage>
class Message1 : public comms::MessageBase<...>
{
using Base = comms::MessageBase<...>;
public:
// Provide names for the fields
COMMS_MSG_FIELDS_NAMES(value1, value2, newValue3);
COMMS_MSG_FIELD_ALIAS(value3, newValue3, member1);
};

The usage of COMMS_MSG_FIELD_ALIAS() above generates the following type and convenience access functions:

template <typename TMessage>
class Message1 : public comms::MessageBase<...>
{
using Base = comms::MessageBases<...>;
public:
// Provide names for the fields
COMMS_MSG_FIELDS_NAMES(value1, value2, newValue3);
// Alias to the member field
using Field_value3 = typename Field_newValue3::Field_member1;
// Access to "newValue3.member1" via "value3" name
auto field_value3() -> decltype(field_newValue3().field_member1())
{
return field_newValue3().field_member1();
}
// Const access to "newValue3.member1" via "value3" name
auto field_value3() const -> decltype(field_newValue3().field_member1())
{
return field_newValue3().field_member1();
}
};

Aliases to Field Names Inside Bundle Fields

Similar to defining aliases to message fields, COMMS library provides an ability to define aliases within bundle field definition using COMMS_FIELD_ALIAS() macro.

class MyBundle : public comms::field::Bundle<...>
{
// (Re)definition of the base class as inner Base type
using Base = comms::field::Bundle<...>;
public:
COMMS_FIELD_MEMBERS_NAMES(member1, member2, member3);
COMMS_FIELD_ALIAS(otherMem1Name, member1);
COMMS_FIELD_ALIAS(otherMem2Name, member2);
};
#define COMMS_FIELD_ALIAS(f_,...)
Generate convinience alias access member functions for other member fields.
Definition: Field.h:390
#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
Bundles multiple fields into a single field.
Definition: Bundle.h:61

Aliases to Extra Transport Field Names Inside Interface

The Common Interface Class class definition may have Extra Transport Values. Aliasing between the extra transport fields can be defined using COMMS_MSG_TRANSPORT_FIELD_ALIAS() macro.

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_FIELD_ALIAS(otherFlagsName, flags);
};
#define COMMS_MSG_TRANSPORT_FIELD_ALIAS(f_,...)
Generate convinience alias access member functions for extra member transport fields.
Definition: Message.h:845

Extra Compile Time Checks

Quite often it is obvious from the protocol specification what minimal length of the serialised message contents is expected to be, and if there are not variable length fields, such as string or list, then maximum serialisation length is also known. It would be wise to slip in compile time checks in message definition as well. There are several static constexpr member functions inherited from comms::MessageBase that can be used:

For example, the implementation of Message2 may be updated as below:

// file Message2.h
#include "comms/comms.h"
#include "MyFieldBase.h" // Defines MyFieldBase common base class for all the fields
namespace my_protocol
{
namespace message
{
template <typename TBase>
class Message2 : public comms::MessageBase<...>
>
{
using Base = comms::MessageBase<...>
public:
static_assert(Base::doMinLength() == 1U, "Unexpected min length");
static_assert(Base::doMaxLength() == 3U, "Unexpected max length");
...
};

Application Specific Customisation

As was mentioned in Fields Definition Tutorial, there may be a need to provide a way for extra application specific customisation for used fields, especially for fields like lists (comms::field::ArrayList) or strings (comms::field::String). By default they use std::vector and std::string respectively as their inner value storage types. They may be un-applicable to some aplications, especially bare-metal ones. In order to solve such problem the message classes are expected to receive extra template parameters, which will be propagated to fields definition.

Recommended Practice

It is recommended to have a separate class / struct called DefaultOptions wich defines relevant inner types to be comms::option::app::EmptyOption (option that does nothing). For example, let's assume that third field in Message1 message is a string. Then the DefaultOptions struct may be defined as

struct DefaultOptions
{
struct message
{
struct Message1Fields
{
using field3 = comms::option::app::EmptyOption; // no extra functionality by default
};
};
};
comms::option::app::EmptyOption EmptyOption
Same as comms::option::app::EmptyOption.
Definition: options.h:1831

NOTE, that inner structure of DefaultOptions in not important, but it is recommended to resemble the full scope of fields, options for which are being prepared. Then the message definition need to be changed to receive and use extra options struct.

#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
>;
};
// Definition of the message itself, described and explained later
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> >
>
{
...
};
} // namespace message
} // namespace my_protocol
Field that represents a string.
Definition: String.h:159

It gives opportunity to the application to define its own single structure of options for all the messages by extending the provided DefaultOptions and overriding selected number inner types with its own extension options.

NOTE, that allowing additional customisation for fields like list (comms::field::ArrayList) and string (comms::field::String) is a must have feature to allow usage of the same protocol definition in bare-metal applications. However, it would be wise to allow extra customisation for all the used fields, even for ones like integral values (comms::field::IntValue) or enum (comms::field::IntValue). The client application developer may want to change the default value of some field, maybe even add or change (override) provided ranges of valid values, force failing of read operation on invalid values, etc... NOTE, that when allowing such customisation of fields, make sure that passed options are listed before the default ones

template <typename... TExtraOpts>
using MyField =
MyFieldBase,
std::uint16_t,
TExtraOpts..., // extra options
comms::option::def::ValidNumValueRange<10, 20> // default range of valid values
>;
Provide range of valid numeric values.
Definition: options.h:956

This is because earlier used options take priority over ones used later.

Depending on the protocol there may be a need to provide additional customisation for messages themselves. For example, when most messages are bi-directional (which will require both read and write operation for every message) except a only a few, that go only one direction (from client to server or the opposite). In this case the generated code will contain implementation for both polymorphic read (comms::MessageBase::readImpl()) and write (comms::MessageBase::writeImpl()). For uni-directional messages some of these function may be redundant, which unnecessary increases the binary size. It may become a problem for bare-metal platforms with limited amount of ROM space. The COMMS library provides options that may suppress automatic generation of some virtual functions by the comms::MessageBase. The available options are:

These options should not be used in the definition of protocol messages, but it would be wise to allow the client application use them when necessary. It can easily achieved using DefaultOptions structure described earlier.

struct DefaultOptions
{
struct message
{
...
};
};

Add passing these options to message definition classes:

namespace my_protocol
{
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

Transport Framing Definition

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 and recognised (based on its ID). The serialised message payload must be wrapped in some kind of transport framing prior to being sent and unwrapped on the other side when received. The Protocol Stack Definition Tutorial page contains detailed information on how define such a frame and make it usable to the client application. Note, that the same protocol may be used over multiple I/O link, every one of which may require different framing. There is no limit on amount of different frames that may be defined.