How to build a Communication Object

This page describes how to build and use Communication Objects with ACE/SMARTSOFT. A general definition of a Communication Object can be found at the PhD thesis of Christian Schlegel "Navigation and Execution for Mobile Robots in Dynamic Environments" (see Hochschule Ulm).

In general: Communication objects parameterize the communication patterns and are transmitted by value. This appears as moving a copy of an object between components. The framework transmits objects by only transmitting the relevant content of a communication object. At the recipient, that content is used to reconstruct a local instance of the appropriate communication object type. The whole procedure is transparent to the user and works like a copy constructor or assignment operator across component boundaries. [phd-thesis] (subsection 5.4.3)

First of all a Communication Object is nothing magic, it is a simple class that can be build completely independent of ACE/SMARTSOFT. However the ACE (ADAPTIVE Communication Environment by Douglas C. Schmidt) framework is still needed. The Communication Object Class does not need any parent classes, however, one can define own parents. From a framework view, a class is a functional Communication Object for ACE/SMARTSOFT if it offers the following three functions:

  • void get(ACE_Message_Block *&msg) const;
  • void set(const ACE_Message_Block *msg);
  • static std::string identifier(void);

Communication objects are regular objects which are decorated by additional member functions for use by the framework. [...] A get-method extracts the relevant data structures and converts them into a platform independent representation for transmission. A set-method converts the platform independent representation back into the original object internal representation. Finally, every communication object type can be identified by a unique name provided by the identifier-method. [phd-thesis] (subsection 5.4.3)

What these functions are exactly for and how they can be implemented is described in the following section.

Three crucial parts of every Communication Object

As mentioned above, a Communication Object does not need any specific polymorphism. Apart from that, there is an approved internal structure that will be described in the next section.

First the sources for the LaserScan communication object example are listed in the following. The Data Structure and the Framework Interface are fully implemented. The user interface is exemplary implemented.

  • SmartTimeStamp.h (Data structure for the TimeStamp Communication Object)
  • CommTimeStamp.h (Header file for TimeStamp Communication Object)
  • CommTimeStamp.cpp (Implementation of the get(...) and set(...) methods of the TimeStamp Communication Object)
  • SmartLaserScan.h (Data structure for the LaserScan Communication Object)
  • CommLaserScan.h (Header file for LaserScan Communication Object)
  • CommLaserScan.cpp (Implementation of the get(...), set(...) and other methods of the LaserScan Communication Object)

Second the crucial parts are described with further details in the next three subsections.

1) Define internal data structure

The first step to do before creating a communication object is to decide on the raw data structure that will be exchanged on the network. It is beneficial to use canonical data format (for example physical SI units like meter, second ...). This is one of the most important steps because it has consequences on user interface and data load on the network. This raw data is the internal data representation of the communication object. Although it can consist of simple data types (like int, float ...) this is not recommendable. The reason is, that ACE performs automatic data-type conversions, which can lead to wrong assumptions about data-type and type-length. The better way is to use ACE_CDR (Common Data Representation) data types (see Table 1) defined in the ACE library. This is the best way to define uniform data types that can be converted from/to every platform dependent data representation (this data types are also used in the ACE-TAO (CORBA) - IDL implementation). Besides the ACE_CDR data types, the raw data can consist of any kind of structures like STL containers (e.g. iterators, lists, vectors, etc.).

ACE_CDR Type-Name Size in bits typical typedef on 32 bit system
Table 1: ACE_CDR data types
ACE_CDR::Boolean 8 bool
ACE_CDR::Octet 8 unsigned char
ACE_CDR::Short 16 ACE_INT16
ACE_CDR::UShort 16 ACE_UINT16
ACE_CDR::Long 32 ACE_INT32
ACE_CDR::ULong 32 ACE_UINT32
ACE_CDR::LongLong 64 platform specific
ACE_CDR::ULongLong 64 platform specific
ACE_CDR::Float 32 platform specific
ACE_CDR::Double 64 platform specific
ACE_CDR::LongDouble 128 platform specific

The best way to create internal data representation is to define a struct that can be stored in a separate header file. This will lead to a structure similar to CORBA-IDL files. To make it more concrete we use an example. For that we define a data structure for a Laser-Scan Communication Object (see Figure 1).

Figure 1: Example for a possible Laser-Scan data structure.

As one can see the data structure does not have to be simple. It is possible to include structures from other Communication Objects (like in this example with TimeStamp and ScanPoint). Additionally the Laser-Scan Communication Object contains a std::list container. The corresponding Communication Object will be described further in the following subsections.

2) Implement framework interface

In general the framework interface is responsible for converting internal data structure into/from a so called CDR-Stream (CDR: Common Data Representation). For that the ACE library offers two classes, namely ACE_OutputCDR and ACE_InputCDR. This classes offer stream operators to shift data into/from stream's internal raw memory buffer. This raw buffer is encapsulated in the ACE_Message_Block class. The ACE_Message_Block stores the data in a uniform way respecting data alignment (data types and sizes) and marshaling (byte order).

This data conversion is implemented in the get(...) and set(...) methods (as described at the beginning). Additionally the identifier() method is used to differ the communication object from others. For that the identifier method should be implemented such that it returns a communication object's name (string) that is unique among all communication objects in a system.

To make the theory more practical we use a simple example (see Figure 2) that shows a communication object for a simple laser-scan. For simplicity reasons we ignore on the one hand the user interface (which will be described later) and on the other hand the data-structure from Laser-Scan (described in the foregoing subsection) is simplified.

Figure 2: Communication Object's framework interface for a simple laser-scan

The first step to do is to implement the identifier method. This could look like the following implementation:

 std::string CommLaserScan::identifier()
 {
   return "CommLaserScan";
 }

As shown in the picture the laser-scan consists of start_angle, a resolution and the actual scan-points (stored as a list of scan-point structs).

Now the methods get(...) and set(...) can be implemented. The get method looks like the following code block:

  void CommLaserScan::get(ACE_Message_Block *&data) const
  {
    // define a local output-stream with default beginning-size of 512k
    ACE_OutputCDR out(ACE_DEFAULT_CDR_BUFSIZE);

    // 1) first we store the direct members
    out << laser_scan.start_angle;
    out << laser_scan.resolution;

    // 2) then we store the list items

    // 2.1) the number of elements in list has to be saved
    ACE_CDR::ULong size = laser_scan.scan.size();
    out << size;

    // 2.2) save all list items
    std::list::const_iterator iter;
    for(iter=laser_scan.scan.begin(); iter != laser_scan.scan.end(); iter++)
    {
      out << iter->index;
      out << iter->distance;
    }

    // finally the stream is returned (as a copy) in the data parameter
    data = out.begin()->clone();
  }

The set method can be implemented as follows:

  void CommLaserScan::set(const ACE_Message_Block *data)
  {
    // get the stream out of message-block
    ACE_InputCDR input(data);
  
    // 1) first we read the direct members
    input >> laser_scan.start_angle;
    input >> laser_scan.resolution;

    // 2) then we read the list items

    // 2.1) the number of elements in list has to be restored
    ACE_CDR::ULong size;
    input >> size;

    laser_scan.scan.clear();

    // 2.2) read all list items
    StructScanPoint scan_point;
    for(ACE_CDR::ULong i=0; i < size; i++)
    {
      input >> scan_point.index;
      input >> scan_point.distance;

      laser_scan.scan.push_back(scan_point);
    }

    // data message-block will be cleared automatically
  }

From here on the communication object can be used in SMARTSOFT/ACE. What is still missing is the user-interface that defines the connection to user code (see next subsection).

3) Define user interface

Once the internal data structure and the framework-interface are defined (see subsections above), the user-interface can be implemented. The user interface consists of getter and setter functions to read/modify the internal data structure. This is an important step during development, because the mapping from user data types to uniform ACE_CDR data types (and vice versa) takes place here. In most cases, it is a direct mapping (for example, int to ACE_CDR::Long or double to ACE_CDR::Double) and no further considerations are necessary.

Again we use the Laser-Scan Communication Object example (see subsections above). This time the laser-scan object will be enriched with a user-interface (see Figure 3).

Figure 3: Complete Communication Object with User-interface for a simple laser-scan.

The user-interface can consist of various setter and getter functions that offer user-specified access to the communication object's internal data. For example the user can choose between laser-scan in Cartesian or Polar coordinates. The core data remains the same because conversion can be done online in the getter/setter functions (see Wiki). Thus the responsibility of the setter and getter functions is to convert data in various formats or semantics (the user could need). Of course the optimum is to reduce conversions as much as possible (which is often the case with Canonical data model).

Typical pitfalls

The experience in development of communication objects shows some typical pitfalls that can be avoided with some general coding guidelines that are summarized in the following.

Copyable objects

The first pitfall is that a communication object must be copyable. This means that an assignment between instances of a communication object must deep-copy the internal data structures. If the data representation is used as described above (e.g. with STL containers) there is nothing else to do. The compiler generates automatically both the assignment operator operator=(...) and the copy constructor. If the communication object implements dynamically allocated data structures that for example are initialized in the constructor and are filled inside of the set(...) method, an assignment operator must be also implemented.

The reason for this is that SMARTSOFT implements a memory management inside of communication ports (e.g. to buffer current (resp. newest) communication object inside of PushNewestClient). In this memory management the communication object is copied using assignment.

Composite Communication Objects

The section above describes how to build a communication object that contains data structures from other communication objects and extends them with further data types. A typical pitfall hereby is that one tries to derive a communication object from an other communication object to inherit its data structures. For example one could derive CommLaserScan from CommTimeStamp. Unfortunately this leads to considerable disadvantages.

First the inheritance of communication objects lead to curious semantics. Inheritance also means that a derived class "is a" specialization of the base class. This would mean for example that a LaserScan is a specialization of the TimeStamp which simply does not make sense. In more complex cases where one derives from several base classes this gets worse.

Second inheritance means that the interface from the base class is inherited in the derived class. For communication objects this would mean that the user interface of the parent communication object is inherited in the derived communication object. This leads to the following problem. The semantic of the user interface in the derived class becomes very unclear. An example for this is if one would derive a LaserScan from a BasePosition communication object (to include current position of the robot base into the current LaserScan). If the LaserScan additionally offer the position where the LaserRangeFinder is mounted on the robot base it would be unclear which position a method get_pos() in the CommLaserScan provides. This can be easily avoided by including (not inherit) the communication object as shown in the class CommLaserScan.h (using getter and setter methods for included communication objects).

Conclusion

Communication objects as described above leads to the following advantages:

  • Communication objects encapsulate the communication mechanism and hide it from the user. Thus the underlying communication layer is transparent for the user and can be exchanged easily.
  • No specific data types (like CORBA data types) are necessary in user space.
  • Marshaling and data type unification is transparent to the user and is done inside of the framework.
  • The user is motivated to define distributed interfaces early.

For further general information see: