cc_tutorial/tutorials/tutorial5 at master · commschamp/cc_tutorial

Tutorial 5

Deeper understanding of <frame>-ing and working with multiple <frame>-s.

Many embedded systems have multiple I/O interfaces for communication with external world. It is also not uncommon for new I/O interfaces been added in the future versions of the hardware. Many developers make a prudent choice and try to reuse the same communication protocol for multiple available I/O links. However, every type of I/O has different reliability built-in and may require different message framing.

For example the TCP/IP link uses checksum in its packets to insure that data is not corrupted and automatically re-requests packets to be sent again if needed. As the result simple framing below is suitable in most cases.

With RS-232 link (or similar) there is a need to take extra measures to identify where the message packet starts and whether the message data was not corrupted on the way with a checksum. It many cases it looks something like this:

SYNC (predefined header) | SIZE | ID | PAYLOAD | CHECKSUM

The whole CommsChampion Ecosystem was designed with clean separation of message definitions with their payloads and the actual framing. It also allows definition of multiple frames in the same protocol schema file(s) and allows the end application to pick at compile time the one that is needed.

In this tutorial we will use two different frames: one for messages sent from client to server and another for the opposite direction. In the process we'll get a deeper understanding of <frame> definition options.

The schema defines two frames. One is for messages being sent by the server to the client.

<frame name="ServerToClientFrame">
    <size name="Size">
        <int name="SizeField" type="uint16" />
    </size>
    <id name="Id" field="MsgId" />
    <payload name="Data" />
</frame>

This is the framing that has been seen in all the previous tutorials. It is simply:

SIZE (2 bytes) | ID (1 byte) | PAYLOAD

The other one for messages being sent from the client to the server is:

<frame name="ClientToServerFrame">
    <sync name="Sync">
        <int name="SyncField" type="uint16" defaultValue="0xabcd" />
    </sync>
    <size name="Size">
        <int name="SizeField" type="uint16" serOffset="2" displayOffset="2" />
    </size>
    <id name="Id" field="MsgId" />
    <payload name="Data" />
    <checksum name="Checksum" alg="crc-ccitt" from="Size" >
        <int name="ChecksumField" type="uint16" />
    </checksum>
</frame>

It represents the following framing:

SYNC (0xabcd - 2 bytes) | SIZE (up until and including CHECKSUM - 2 bytes) | ID (1 byte) | PAYLOAD | CHECKSUM (crc-ccitt - 2 bytes)

The relevant generated code reside in include/tutorial5/frame/ServerToClientFrame.h and include/tutorial5/frame/ClientToServerFrame.h

To properly understand the schema definition above and the implementation implications let's go through the important points one by one.

First of all, every <frame> must define internal so called layers (<sync>, <size>, etc...). Every such layer handles only one specific value inside the message frame. To properly describe the length and type of such value, the layer needs to define suitable inner field:

<sync name="Sync">
    <int name="SyncField" type="uint16" defaultValue="0xabcd" />
</sync>

SIDE NOTE: In case the <frame> XML node has properties other than layers defined as XML children, then the layers themselves need to be defined inside <layers> XML node.

<frame name="ClientToServerFrame">
    <description>
        This frame is used for messages being sent from client to server.
    </description>
    <layers>
        <sync name="Sync">
            <int name="SyncField" type="uint16" defaultValue="0xabcd" />
        </sync>
        ...
    </layers>
</frame>

Similar with the definition of the layer itself. If it has some other non inner field definition properties defined as XML children, then the field definition needs to be defined as a child of <field> XML node:

<sync name="Sync">
    <description>
        Synchronization bytes to mark beginning of the message.
    </description>
    <field>
        <int name="SyncField" type="uint16" defaultValue="0xabcd" />
    </field>
</sync>

Let's take a look at the defined layers of ClientToServerFrame (from include/tutorial5/frame/ClientToServerFrame.h) one by one.

<payload> Layer

The <payload> layer represents message payload (serialized fields). It is the only layer that doesn't have any inner field that represents framing value.

Such layer is implemented by extending or aliasing comms::frame::MsgDataLayer.

using Data =
    comms::frame::MsgDataLayer<
        ...
    >;

<id> Layer

The <id> layer represents numeric message ID.

<id name="Id" field="MsgId" />

Note that the MsgId field is defined in the global space and is used to enumerate messages. The CommsDSL allows reference of such field with field property rather than defining a field as XML child element.

Such layer is implemented by extending or aliasing comms::frame::MsgIdLayer, which is responsible to read the numeric message ID and create (allocate) appropriate message object.

template <typename TMessage, typename TAllMessages>
using Id =
    comms::frame::MsgIdLayer<
        tutorial5::field::MsgId<TOpt>,
        TMessage,
        TAllMessages,
        Data,   // <-- Data layer wrapped
        typename TOpt::frame::ClientToServerFrameLayers::Id
    >;

Note, that COMMS Library implement message framing by folding layers, where one layer wraps another and keeps the latter as its private data member. Such architecture allows assembling a required framing out of multiple building blocks as well as having any extra logic before and after read/write operations are forwarded the the next layer for processing.

The definition above receives two template parameters TMessage and TAllMessages. The first one (TMessage) is expected to be a type of common interface class (descendant of comms::Message) of all the message types, while second (TAllMessages) is std::tuple of all the input message types (descendants of comms::MessageBase), which the ID layer is expected to recognize and create relevant object during read operation.


SIDE NOTE: The last template parameter passed to the comms::frame::MsgIdLayer is actually a compile time configuration parameter that can be used by the end-application to configure the behavior of the layer, such as replace default dynamic memory allocation of message objects with in-place allocation more suitable for bare-metal development. Such compile time configuration is a subject for another a bit later tutorial.


<size> Layer

The <size> layer represent a remaining data length until end of the <payload> layer NOT including the length of the size field itself and NOT including any values after <payload>.

<size name="Size">
    <int name="SizeField" type="uint16" serOffset="2" displayOffset="2" />
</size>

Note, that in this particular tutorial the SIZE value in the framing represents remaining length until end of the message including checksum (which follows the <payload>). To satisfy this requirement, the serOffset="2" property has been used to add extra 2 bytes (length of the checksum) to the serialized value.

There are protocols that include length of the size field itself as the value of the SIZE in the protocol framing. In such case the serOffset property also needs to be used.

Usage of the displayOffset property can be used the force adding the same offset to the value displayed in CommsChampion Tools and it's not really relevant to this tutorial.

The <size> layer is implemented by extending or aliasing comms::frame::MsgSizeLayer.

template <typename TMessage, typename TAllMessages>
using Size =
    comms::frame::MsgSizeLayer<
        typename SizeMembers::SizeField,
        Id<TMessage, TAllMessages> // <-- Id layer wrapped
    >;

Just like described earlier the Size layer type definition receives the type of the layer it wraps (Id) as its template parameter.

<checksum> Layer

The <checksum> layer is used to define checksum information that needs to be calculated.

<checksum name="Checksum" alg="crc-ccitt" from="Size" >
    <int name="ChecksumField" type="uint16" />
</checksum>

The CommsChampion Ecosystem has a list of supported built-in checksum algorithms which can be specified using alg property. Please refer to CommsDSL specification for a full list. In this particular tutorial CRC-CCITT is used.


SIDE NOTE: It is possible to implement and add usage of custom checksum calculation algorithm, which is a bit out of scope for this particular tutorial. This subject will be covered in one of the later tutorials.


Please also pay attention to usage of from property in <checksum> layer definition. It specifies from which layer the checksum calculation needs to be performed. In the case of this tutorial it's from SIZE layer until the CHECKSUM itself. The CommsDSL also supports usage of <checksum> layer as prefix to the area on which the checksum needs to be calculated. In this case the until property needs to be used.

The suffix <checksum> layer is implemented by extending or aliasing comms::frame::ChecksumLayer or comms::frame::ChecksumPrefixLayer depending on whether the checksum follows or precedes the data used for checksum calculation.

template <typename TMessage, typename TAllMessages>
using Checksum =
    comms::frame::ChecksumLayer<
        typename ChecksumMembers::ChecksumField,
        comms::frame::checksum::Crc_CCITT,
        Size<TMessage, TAllMessages>
    >;

Note, that the C++ class of Checksum layer needs to wrap all the other layers on which the checksum value is calculated. That's the reason while the Checksum wraps the Size.

<sync> Layer

The <sync> layer is used to define synchronization prefix. The defaultValue property of the field needs to be used to define the synchronization value.

<sync name="Sync">
    <int name="SyncField" type="uint16" defaultValue="0xabcd" />
</sync>

The <sync> layer is implemented by extending or aliasing comms::frame::SyncPrefixLayer.

template <typename TMessage, typename TAllMessages>
using Sync =
    comms::frame::SyncPrefixLayer<
        typename SyncMembers::SyncField,
        Checksum<TMessage, TAllMessages>
    >;

Note, that there is no real need to use validValue and/or failOnInvalid properties for the definition of the SyncField to force the read operation to fail on invalid value. The implementation of the comms::frame::SyncPrefixLayer just compares the read field with the default constructed one and fails the read operation if they are not equal. During write operation the layer will just invoke write() member function on the default constructed field, which will write the correct value thanks to usage of defaultValue property in the field definition.

Full Frame

The last (outermost) layer is actually used as a transport frame.

template <typename TOpt = tutorial5::options::DefaultOptions>
struct ClientToServerFrameLayers
{
    ...

    template<typename TMessage, typename TAllMessages>
    using Stack = Sync<TMessage, TAllMessages>;
};

template <
   typename TMessage,
   typename TAllMessages = tutorial5::input::AllMessages<TMessage>,
   typename TOpt = tutorial5::options::DefaultOptions
>
class ClientToServerFrame : public
    ClientToServerFrameLayers<TOpt>::template Stack<TMessage, TAllMessages>
{
public:
    COMMS_FRAME_LAYERS_NAMES(
        data,
        id,
        size,
        checksum,
        sync
    );
};

As the result the documentation of the last layer type (comms::frame::SyncPrefixLayer for the current example) can be used for reference on available framing API.

The final definition of ClientToServerFrame frame class uses COMMS_FRAME_LAYERS_NAMES() macro to assign names for the defined layers. For every name X the macro generates Layer_X type and layer_X() member function to allow access to it if needed. This particular tutorial doesn't have such a need, so these functions are not really used.

Now, let's take a look at the sending and processing code inside the src/ClientSession.cpp.

void ClientSession::sendMessage(const Message& msg)
{
    std::vector<std::uint8_t> output;
    ...
    auto writeIter = std::back_inserter(output);

    auto es = m_clientToServerFrame.write(msg, writeIter, output.max_size());
    if (es == comms::ErrorStatus::UpdateRequired) {
        auto updateIter = output.begin();
        es = m_clientToServerFrame.update(updateIter, output.size());
    }

    if (es != comms::ErrorStatus::Success) {
        assert(!"Write operation failed unexpectedly");
        return;
    }
    ...
}

The important code is listed above. Note, that the write operation uses output iterator to write the code (std::back_inserter(output)) which cannot be returned back to and used to re-read the written data in order to calculate the checksum value before it's been written. As the result the call to m_clientToServerFrame.write(...) will put some dummy (0) two bytes as the checksum and return commms::ErrorStatus::UpdateRequired to indicate that the write() operation is not complete. The call to update() with random access iterator needs to follow. It will be used to re-read the written data as well as overwrite the dummy checksum with a correct value.

Similar situation may occur when the interface class doesn't expose polymorphic length() member function (the comms::option::app::LengthInfoInterface option has NOT been provided). In such case when SIZE value needs to be written the proper value cannot be retrieved (because length of message payload is not known). In this case the comms::frame::MsgSizeLayer will write a dummy value and force a return of commms::ErrorStatus::UpdateRequired. After that when the update() is called, the layer will calculate size of the written code and update previously written dummy value with the correct one.


SIDE NOTE: In the example above the update() functionality doesn't require any knowledge about the recently written message to be able to analyse the recently written data and update values where needed. However, in some cases the access to previously written message needs to be provided. The COMMS Library provides such overloaded update() member function which receives a reference to the message object as its first parameter.


Note that this particular tutorial focuses on the deeper understanding of message framing rather than messages and their fields. All the exchanged messages are defined not to contain any payload:

<message name="Msg1" id="MsgId.M1" displayName="^Msg1Name" />
<message name="Msg2" id="MsgId.M2" displayName="^Msg2Name" />
<message name="Msg3" id="MsgId.M3" displayName="^Msg3Name" />

In this particular case there is no real need to do a separate handle() message for every message type. There is only one common function:

void ClientSession::handle(Message& msg)
{
    std::cout << "Received \"" << msg.name() << "\" with ID=" << (unsigned)msg.getId() << '\n';
    ...
}

Note, that the compiler still looks for the best match of the handling function to invoke when compiling code relevant for the message dispatch. In this particular case there is only one function to choose from. However, if you add a handling function with better type match say void ClientSession::handle(Msg1& msg), then this function will be called to handle Msg1 message instead of common one when the code is recompiled.

Summary

  • The CommsChampion Ecosystem allows clear separation of the protocol messages definition and the transport framing.
  • The transport framing is defined using <frame> XML node.
  • The protocol schema allows definition of multiple transport frames and the generated code allows the end application to select required one at compile time.
  • Every <frame> uses internal layers to specify transport fields and their roles.
  • The generated C++ code of the frame(s) resides in include/<namespace>/frame folder and uses classes from comms::frame namespace to define the layers.
  • The defined framing layers wrap one another, as the result the outermost layer is used to handle the whole transport framing.
  • For available frame API reference open the documentation of the outermost layer type.
  • The polymorphic behavior of the common interface class may influence the ability of the frame to perform its write() operation.
  • When write operation returns commms::ErrorStatus::UpdateRequired use random access iterator to perform update() operation.

Read Previous Tutorial <-----------------------> Read Next Tutorial