Serialization and Application Layer Protocol Framework

The application layer protocol framework provides serialization and deserialization functionality for message-based communication among different devices. It implements a consistent mechanism for generating and parsing messages from a single message definition. This message definition can be used as a message parser and/or a message generator.

The serialization format (e. g. binary format, ASCII) can be flexibly changed. For binary data exchange, the serialization format takes care of endianness conversion.

As with these mechanisms, the message producers and the message consumers share the same message definition, the risk for discrepancies between the communication partners is eliminated.

Example

This example demonstrates how easy an RPC-like mechanism for data communication between multiple communication partners can be implemented:

It stipulates a target with loop controller functionality that can be parameterized via a host application. Thus, host and target need to implement a suitable application protocol. Messages that are generated by the host must be parsed by the target and vice versa.

Our example will deal with the following messages:

  • Modification of the P, I and D controller parameters (sender: host, receiver: target)
  • Acknowledgement of this message (sender: target, receiver: host)

With the redBlocks serialization classes, a message can be generated and parsed from a single message definition. For our example we choose a binary data format that transmits the data in network byte order.

The message for modifying the P, I, and D controller parameters shall consist of a message id with value 0 and a 16 bit integer for each of the P, I and D controller parameters:

class SetPidValuesMessage : public MessageProtocol::TMessage<0>
{
  public:
    u16 mParamP;
    u16 mParamI;
    u16 mParamD;

    virtual bool process(Serializer::IProcessor& p)
    {
      return
        p.process( TField<Format::FieldU16> ( mParamP ) ) &&
        p.process( TField<Format::FieldU16> ( mParamI ) ) &&
        p.process( TField<Format::FieldU16> ( mParamD ) );
    }
};

The message id is specified as a template argument to the class template TMessage from which SetPidValuesMessage is derived (in our case 0). The member variables mParamP, mParamI, mParamD carry the values that are serialized or have been parsed from a received message. The method process() defines the message format, i. e. the format of each data field and their order.

The acknowledgement message definition uses the same concept. Its message id is 1 and it consists of a boolean data field that indicates whether the operation was successful or not.

class SetPidValuesAckMessage : public MessageProtocol::TMessage<1>
{
public:
  bool mOk;
  virtual bool process(Serializer::IProcessor& p)
  {
    return
p.process( TField<Format::FieldBool> ( mOk ) );
  }
};

The format that is used for the data fields can easily be replaced by changing the definition for the identifier Format. In our example we choose the BinaryNetworkFormat.

typedef redBlocks::Serialization::BinaryNetworkFormat Format;

When the message definition is in place, a message can conveniently be generated and sent. The redBlocks class MessageProtocol takes care of the complete serialization process.

The following code snippet shows how to do this:

SetPidValuesMessage msg;
msg.mParamP = 20;
msg.mParamI = 40;
msg.mParamD = 60;

u16 msgSize = MessageProtocol::getSerializedSize(msg);
Protocol::MessageSendContainer* msgSendContainer = protocol.createMessageSendContainer( msgSize );
if (MessageProtocol::serialize( *msgSendContainer, msg))
{
  msgSendContainer->send();
}

The above code assumes that an instance with the name protocol exists via which a message container can be obtained, filled and sent. For this application layer functionality it is irrelevant which underlying protocol is used (UDP, TCP or any other proprietary protocol stack).

Processing incoming messages is equally convenient. For each message a so-called message processor needs to be defined like so:

class SetPidValuesMessageProcessor : public MessageProtocol::TMessageProcessor< SetPidValuesMessage >
{
public:
  virtual void processMessage( )
  {
    LOG_DEBUG("Received PID values: " << mMsg.mParamP << ", " << mMsg.mParamI << ", " << mMsg.mParamD);
  }
};

Within the method processMessage() the message fields can be accessed via the member variables of mMsg - already filled with the deserialized values of the received message. In the above example, the message data is only used for logging.

The message processors that are defined like shown above, can be used together with a message dispatcher in a chain-of-responsibility pattern that works like this: For every incoming message the message id is parsed, the corresponding message processor selected, the message deserialized and the message processor's method processMessage() called.

Message dispatcher configuration during system initialization:

using redBlocks::Collection::TArrayDynamicAlloc;

// ...

typedef MessageProtocol::Dispatcher Dispatcher;
TArrayDynamicAlloc< Dispatcher* > *processors = new TArrayDynamicAlloc< Dispatcher* >(numProcessors);
processors->addLastElement( new SetPidValuesMessageProcessor() );
// ... add more message processors

The received message can be easily distributed like this afterwards:

Protocol::MessageRecvContainer* msgRecvContainer =  protocol.getMessageRecvContainer();
MessageProtocol::dispatchMessage( *msgRecvContainer, processors );

Wrap-Up

The demonstrated redBlocks approach has the follwoing advantages:

  • Based only on the message definitions, messages can be serialized and deserialized by the framework without any further effort.
  • The message format can be transparently exchanged.
  • Each message definition can be used by the message producers and the message parsers. Thus, changes to the message definition that exists exactly once will automatically affect all communication partners, i. e. only one place in the code needs to be modified.
  • Serialization and deserialization happens without the need to copy the message content, i. e. the generated code is extremely compact and efficient.
  • The underlying protocol layers are totally transparent to the application layer protocol.

The redBlocks application layer protocol framework is easy to use. Only the following steps are required:

  • Message definition.
  • Definition of message processors and adding them to a collection class.
  • For message parsing the method MessageProtocol::dispatchMessage() needs to be invoked.
  • In order to serialize a message to a message container, the methods MessageProtocol::getSerializedSize() and MessageProtocol::serialize() can be used.