Skip to content

Messages

Emanuele Giacomini edited this page Jan 23, 2020 · 4 revisions

BE-MESP Messages

Message interface

To use the mesh network, a form of message is used. Each message is defined by two sectors, namely:

  • Header
  • Payload

Header

The header contains useful information regarding the message. The following elements are found in the header:

  1. destination device address (6 bytes)
  2. source device address (6 bytes)
  3. ID (1 byte)
  4. Number of hops (1 byte)
  5. Sequence number of the current packet (1 byte)
  6. Payload size (namely psize) (1 byte)

Some of these members are not yet used, while ID and psize are currently used to parse the message during deserialization processes.

Payload

The payload contains the data units that need to be shared between the nodes. Obviously multiple data structures are already implemented, however the procedure needed to implement other data structures is quite straightforward.

Howto: create new messages

To implement other messages we should first understand the process in which the message is created, serialized and then deserialized.

Message definition

Messages follow a hierarchical structure. The main class is called MessageHeader and it represents the fundamental component of a message (Also a header without a payload may be considered a message in itself). Please refer to the bemesh_messages_v2.hpp header for additional Infos.

#define MESSAGE_HEADER_DATA_SIZE 16
class MessageHeader {
  protected:
    dev_addr_t m_dest_addr;
    dev_addr_t m_src_addr;
    uint8_t m_id;
    uint8_t m_hops;
    uint8_t m_seq;
    uint8_t m_psize;
  public:
    MessageHeader();
    MessageHeader(dev_addr_t t_dest, dev_addr_t t_src, uint8_t t_id,
	      uint8_t t_hops, uint8_t t_seq, uint8_t t_psize);
    MessageHeader(const MessageHeader& h);

Another type of header often useful for passing arrays is the IndexedMessage header, which inherits the MessageHeader class.

class IndexedMessage : public MessageHeader {
protected:
  uint8_t m_entries;
 public:
   IndexedMessage();
   IndexedMessage(std::size_t t_entries, dev_addr_t t_dest, dev_addr_t t_src, uint8_t t_id,
              uint8_t t_hops, uint8_t t_seq, uint8_t t_psize);

All following messages will either inherit MessageHeader or IndexedMessage classes, based on the type of payload currently used.

General guidelines for payload definition

In order to serialize the message these guidelines should be followed:

  • No pointers to data classes defined outside the message space, hence most of dynamic containers.
  • Use arrays! C++ comes with a particular container named std::array<T, std::size_t>. We are going to use this a lot since it is self-contained (no splitting between the container and the data, like for vectors, maps, etc)
  • Each message should be contained in 255 bytes max due to GATT limitations, and because of internal structure of the header, which allows a payload size of at most 255 bytes.

Serialization

The MessageHeader class implements an important method called serialize used to create a stream of bytes representing the current object

void serialize(std::ostream&) const;

The MessageHeader and IndexedMessage already implement their own serialize functions in order to parse correctly the header section of the message ( plus additional overheads needed for deserialization).

Defining your own message, you need to implement the serialize function for its payload only. For instance, lets take a look at how serialize is implemented in the RoutingUpdateMessage:

void RoutingUpdateMessage::serialize(std::ostream&out) const {
   IndexedMessage::serialize(out);
   for(int i=0;i<m_entries;++i) {
     out.write(reinterpret_cast<const char*>(&m_payload[i]), sizeof(routing_update_t));
   }
}

To write your data inside the stream, the write function must be used, along with the _reinterpret_cast<const char*>() operator, which nullifies possible chances to fragmentation inside the byte stream. The size of the data structure must also be placed carefully through the sizeof operator. Also notice how only the used slots of the array are streamed consistently with the m_entries parameter of the IndexedMessage parent, instead of the whole array (this will save us some space in the stream).

Unserialization

In order to unserialize any message, a static function is used, namely MessageHeader* MessageHeader::unserialize(std::istream& istr) which basically works in 3 steps:

  • Read the stream in order to get the ID of the message
  • Find the correct class constructor through a map
  • invoke and return the custom create method of the new object

You don`t need to worry about the first step since unserialize will take care of it, however, what you need to do is to extend the _serial_ctor_map found in bemesh_messages_v2.cpp file:

static std::map<uint8_t, MessageHeader*> _serial_ctor_map =
{
  {ROUTING_DISCOVERY_REQ_ID, new RoutingDiscoveryRequest()},
  {ROUTING_DISCOVERY_RES_ID, new RoutingDiscoveryResponse()},
  {ROUTING_UPDATE_ID, new RoutingUpdateMessage()},
};

By providing a new pair {ID, ctor}. Please define an argument-less constructor since no data regarding the message is available yet at this point.

At this point, you need to implement the <yourMessageType*> create(std::istream&) function, which must follow the exact order used for serializing data. Plus you have to copy the header since all the message is parsed through your function.

Let's see how create is built for RoutingUpdateMessage:

RoutingUpdateMessage* RoutingUpdateMessage::create(std::istream& istr) {
  // Read header
  istr.read(reinterpret_cast<char*>(&m_dest_addr), sizeof(dev_addr_t));
  istr.read(reinterpret_cast<char*>(&m_dest_addr), sizeof(dev_addr_t));
  istr.read(reinterpret_cast<char*>(&m_id), sizeof(m_id));
  istr.read(reinterpret_cast<char*>(&m_hops), sizeof(m_hops));
  istr.read(reinterpret_cast<char*>(&m_seq), sizeof(m_seq));
  istr.read(reinterpret_cast<char*>(&m_psize), sizeof(m_psize));
  // Read the payload
  istr.read(reinterpret_cast<char*>(&m_entries), sizeof(m_entries));
  for(int i=0;i<m_entries;++i) {
    routing_update_t temp_entry;
    istr.read(reinterpret_cast<char*>(&temp_entry), sizeof(routing_update_t));
    m_payload[i]=temp_entry;
  }
  return this;
}

Once the steps are done, you should be able to use your message with the same methods offered by the MessageHandler class.

Finally, I'm leaving an example of RoutingUpdateMessage definition for you to follow:

#define ROUTING_UPDATE_ID 0x02
#define ROUTING_UPDATE_ENTRIES_MAX 13
class RoutingUpdateMessage : public IndexedMessage {
  std::array<routing_update_t, ROUTING_UPDATE_ENTRIES_MAX> m_payload;
public:
  RoutingUpdateMessage();
  RoutingUpdateMessage(dev_addr_t t_dest, dev_addr_t t_src,
		   std::array<routing_update_t,
		   ROUTING_UPDATE_ENTRIES_MAX> t_payload,
		   std::size_t t_pentries);
  std::array<routing_update_t, ROUTING_UPDATE_ENTRIES_MAX> payload(void);

  // Serialization
  void serialize(std::ostream&) const;
  RoutingUpdateMessage* create(std::istream&);
};

Here lies the list of possible messages that can be sent through the BE-MESP network.

Elementar Types

There are two main types of headers that a message can inherit:

  • MessageHeader:
    • Generic message header. Used to generate any generic purpose message. It contains the following parameters:
      • Target Address
      • Source Address
      • Message ID
      • No. Hops
      • Sequence Number
      • Payload Size
  • IndexedMessage:
    • Message header used for indexable payload (ie. transmission of an array). It inherits all the header information of MessageHeader, but also contains a new variable for entries number in the payload:
      • MessageHeader data
      • Entries

Routing Messages

The following messages are used for the routing procedure.

  • RoutingDiscoveryRequest(MessageHeader):
    • Request for the routing discovery procedure. The target should answer this request with a RoutingDiscoveryResponse. This message contains no payload as it's only purpose is to notify another device of the given request.
  • RoutingDiscoveryResponse(IndexedMessage):
    • Response for a received RoutingDiscoveryRequest.
      • std::array<routing_params_t, ROUTING_DISCOVERY_RES_ENTRIES_MAX>
  • RoutingUpdateMessage(IndexedMessage):
    • Used to notify neighbours of recent updates in the routing table of a device.
      • std::array<routing_update_t, ROUTING_UPDATE_ENTRIES_MAX>
  • RoutingSyncMessage(IndexedMessage):
    • Used to synchronize the vector clock of the network devices. Each entry in the payload represent a cell of the vector clock of the sender.
      • std::array<uint8_t, ROUTING_SYNC_ENTRIES_MAX>
  • RoutingPingMessage(MessageHeader):
    • Used for flooding in the network. Mostly used for research purpose.
      • pong_flag: 0 if the message is a PING. 1 if the message is a PONG.