Part V: Developing and Testing the ROS Message Endpoint

[Author’s note, July 28, 2010: Introduced Boost::shared_ptr to manage reference to message endpoint so as to enable postponed construction, as will be required in Part VI.]

While this series (Part I, II, III, IV) has been specifically written to address writing well-designed packages for ROS, we’ve actually seen very little of ROS itself thus far. In fact, outside of the use of roscreate to generate the package basics and sensor_msgs::LaserScan for communicating laser scan data from the reader up to the application services layer, there’s been no indication that this application was actually intended to work with ROS now or ever. Ironically, this is exactly what we’d expect to see in a well designed ROS package.

Each layer that we’ve developed – as initially outlined in Part I – is logically separated from each other’s context of responsibility. To illustrate, the upper layers do not directly depend on “service” layers, such as message endpoints. Instead, the lower layers depend on abstract service interfaces declared in the upper layers. This dependency inversion was enabled in Part IV with the creation of ILaserScanEndpoint, a separated interface. If all of this dependency inversion and separated interface mumbo-jumbo has your head spinning at all, take some time to delve deeper into this subject in Dependency Injection 101.

While the actual message endpoint interface was created, only a test double was developed for testing the application service layer’s functionality. Accordingly, in this post, the concrete message endpoint “service,” which implements its separated interface, will be developed and tested. That’s right…we’ll finally actually talk to ROS! You can skip to the chase and download the source for this post.

Before digging into the code, it’s important to take a moment to better understand the purpose and usefulness of the message endpoint. The message endpoint encapsulates communications to the messaging middleware similarly to how a data repository encapsulates communications to a database. By encapsulating such communications, the rest of the application (ROS package, in our case) may remain blissfully oblivious to details such as how to publish messages to a topic or translate between messages and domain layer objects.

This separation of concerns helps to keep the application cleanly decoupled from the messaging middleware. Another benefit of this approach is enabling the development and testing of nearly the entirety of the application/package before “wiring” it up to the messaging middleware itself. This typically results in more reusable and readable code. If you haven’t already, I would encourage you to read the article Message-Based Systems for Maintainable, Asynchronous Development for a more complete discussion on message endpoints.

Onward!

Target Class Diagram

The following diagram shows what the package will look like after completing the steps in this post…it’s beginning to look oddly familiar to the package diagram discussed in Part I of this series, isn’t it? If you’ve been following along, most of the elements have already been completed; only the concrete LaserScanEndpoint and LaserScanEndpointTests will need to be introduced along with a slight modification to the TestRunner.

1. Setup the Package Skeleton, Domain Layer and Application Services Layer

If not done already, follow the steps in Part II, Part III, and Part IV to create the package and develop/test the domain and application service layers. (Or just download the code from Part IV as a starting point to save some time.)

2. Create the message endpoint header class.

Create src/message_endpoints/LaserScanEndpoint.hpp containing the following code:

// LaserScanEndpoint.hpp
 
#ifndef GUARD_LaserScanEndpoint
#define GUARD_LaserScanEndpoint
 
#include <ros/ros.h>
#include "sensor_msgs/LaserScan.h"
#include "ILaserScanEndpoint.hpp"
 
namespace ladar_reporter_message_endpoints
{
  class LaserScanEndpoint : public ladar_reporter_core::ILaserScanEndpoint
  { 
    public:
      LaserScanEndpoint();
 
      void publish(const sensor_msgs::LaserScan& laserScan) const;
 
    private:
      // Create handle to node
      ros::NodeHandle _ladarReporterNode;
 
      ros::Publisher _laserReportPublisher;
  };
}
 
#endif /* GUARD_LaserScanEndpoint */

The message endpoint header simply implements ILaserScanEndpoint and sets up handlers for holding the ROS NodeHandle and Publisher. The more interesting bits are found in the implementation details…

3. Create the message endpoint implementation class.

Create src/message_endpoints/LaserScanEndpoint.cpp containing the following code:

// LaserScanEndpoint.cpp
 
#include <ros/ros.h>
#include "sensor_msgs/LaserScan.h"
#include "LaserScanEndpoint.hpp"
 
namespace ladar_reporter_message_endpoints
{
  LaserScanEndpoint::LaserScanEndpoint() 
    // Setup topic for publishing laser scans to
    : _laserReportPublisher(
      _ladarReporterNode.advertise<sensor_msgs::LaserScan>("laser_report", 100)) { }
 
  void LaserScanEndpoint::publish(const sensor_msgs::LaserScan& laserScan) const {
    _laserReportPublisher.publish(laserScan);
    ros::spinOnce();
    ROS_INFO("Published laser scan to laser_report topic with angle_min of: %f", laserScan.angle_min);
  };
}

As you can see, there’s really not much to the actual publication process…which is what we were hoping for. The message endpoint should simply be a light way means to send and receive messages to/from the messaging middleware. This message endpoint does so as follows:

  • The constructor initializes the publisher by advertising on a topic with the name of “laser_report.”
  • The publish() function simply takes the received laserSan and moves it along to be published via ROS. Although not necessary for this specific package code, the call to spinOnce() will be important when the package has callbacks based on messages received. (See http://www.ros.org/wiki/ROS/Tutorials/WritingPublisherSubscriber(c%2B%2B) for more details.)

4. Configure CMake to Include the Header and Implementation

With the header and implementation classes completed, we need to make a couple of minor modifications to CMake for their inclusion in the build.

  1. Open /ladar_reporter/CMakeLists.txt. Within the include_directories statement, add an include for the following directory to include the concrete message endpoint header:
    "src/message_endpoints"
  2. Open /ladar_reporter/src/CMakeLists.txt. Add an inclusion for the message_endpoints directory at the end of the file:
    add_subdirectory(message_endpoints)
  3. Now, in order to create the message endpoints class library itself, a new CMake file is needed under /ladar_reporter/src/message_endpoints. Accordingly, create a new CMakeLists.txt under /ladar_reporter/src/message_endpoints, containing the following:
    # Create the library
    add_library(
      ladar_reporter_message_endpoints
      LaserScanEndpoint.cpp
    )

5. Build the message endpoints Class Library

In a terminal window, cd to /ladar_reporter and run make. The class library should build and link successfully.

Like with everything else thus far…it’s now time to test our new functionality.

6. Unit Test the LaserScanEndpoint Functionality

While testing up to this point has been pretty straight-forward, we now need to incorporate ROS package initialization within the test itself.

  1. Our package is going to act as a single ROS node; accordingly, we need to modify /ladar_reporter/test/TestRunner.cpp to initialize ROS within the package and to register itself as a node which we’ll call “ladar_reporter.” While I’m hard-coding the node name within the test itself, you may want to consider putting the name into a config file as it’ll likely be referenced in multiple places; having it centralized within a config file gets rid of the magic string, making it easier to change while reducing the likelihood of typing it wrong in some hard-to-track-down spot.

    Open /ladar_reporter/test/TestRunner.cpp and modify the code to reflect the following:

    #include <gtest/gtest.h>
    #include <ros/ros.h>
     
    // Run all the tests that were declared with TEST()
    int main(int argc, char **argv){
      testing::InitGoogleTest(&argc, argv);
     
      // Initialize ROS and set node name
      ros::init(argc, argv, "ladar_reporter");
     
      return RUN_ALL_TESTS();
    }
  2. Create a new testing class, /ladar_reporter/test/message_endpoints/LaserScanEndpointTests.cpp:
    // LaserScanEndpointTests.cpp
     
    #include <gtest/gtest.h>
    #include "sensor_msgs/LaserScan.h"
    #include "LaserScanEndpoint.hpp"
    #include "LaserScanReportingService.hpp"
     
    using namespace ladar_reporter_application_services;
    using namespace ladar_reporter_message_endpoints;
     
    namespace ladar_reporter_test_message_endpoints
    {
      // Define unit test to verify ability to publish laser scans 
      // to ROS using the concrete message endpoint.
      TEST(LaserScanEndpointTests, canPublishLaserScanWithEndpoint) {
        // Establish Context
        LaserScanEndpoint laserScanEndpoint;
        sensor_msgs::LaserScan laserScan;
     
        // Give ROS time to fully initialize and for the laserScanEndpoint to advertise
        sleep(1);
     
        // Act
        laserScan.angle_min = 1;
        laserScanEndpoint.publish(laserScan);
     
        laserScan.angle_min = 2;
        laserScanEndpoint.publish(laserScan);
     
        // Assert
        // Nothing to assert other than using terminal windows to 
        // watch publication activity. Alternatively, for better testing, 
        // you could create a subscriber and subscribe to the reports 
        // You could then track how many reports were received and 
        // assert checks, accordingly.
      }
     
      // Define unit test to verify ability to leverage the reporting 
      // service using the concrete message endpoint. This is more of a 
      // package integration test than a unit test, making sure that all 
      // of the pieces are playing together nicely within the package.
      TEST(LaserScanEndpointTests, canStartAndStopLaserScanReportingServiceWithEndpoint) {
        // Establish Context
        boost::shared_ptr<LaserScanEndpoint> laserScanEndpoint =
          boost::shared_ptr<LaserScanEndpoint>(new LaserScanEndpoint());
        LaserScanReportingService laserScanReportingService(laserScanEndpoint);
     
        // Give ROS time to fully initialize and for the laserScanEndpoint to advertise
        sleep(1);
     
        // Act
        laserScanReportingService.beginReporting();
        sleep(4);
        laserScanReportingService.stopReporting();
     
        // Assert
        // See assertion note above from 
        // LaserScanEndpointTests.canPublishLaserScanWithEndpoint
      }
    }

    The comments within the test class above should clarify what is occurring. But in summary, the canPublishLaserScanWithEndpoint test bypasses all of the layers and tests the publishing of messages directly via the message endpoint. The canStartAndStopLaserScanReportingServiceWithEndpoint test takes this much further and injects the LaserScanEndpoint message endpoint into the LaserScanReportingService application service and starts/stops the laser scan reprorting, accordingly. This latter test should be seen more as an integration test rather than a unit test as it tests the results of all of the layers working together.

  3. Open /ladar_reporter/test/CMakeLists.txt. Within the rosbuild_add_executable statement, add the following to include the message endpoint tests in the build:
    ./message_endpoints/LaserScanEndpointTests.cpp
  4. While we’re at it, we’ll also need to link the new message_endpoints class library to the unit testing executable; accordingly, also within /ladar_reporter/test/CMakeLists.txt, modify target_link_libraries to reflect the following:
    # Link the libraries
    target_link_libraries(
      ladar_reporter_tests
      ladar_reporter_application_services
      ladar_reporter_message_endpoints
      # Important that core comes after application_services due to direction of dependencies
      ladar_reporter_core
    )
  5. Verify that everything builds OK by running make within a terminal from the root folder, /ladar_reporter.
  6. Now for the fun part…time to run our tests with ROS running. The steps will follow closely to those described in the ROS tutorial, Examining Pulisher/Subscriber:
    1. Open a new terminal and run roscore to begin ROS
    2. In the terminal that you’ve been building in…
      make
      cd bin
      ./ladar_reporter_tests

    With a little luck, you should see a few messaging being published to ROS while running the unit tests. And just to prove it…

    1. With roscore still running, open a third terminal window and run rostopic echo /laser_report. You’ll likely be warned that the topic is not publishing yet…let’s change that.
    2. Back in your original terminal, the one you ran the unit tests in, rerun ./ladar_reporter_tests. You should now being seeing laser scans being echoed to the third terminal window for the LaserScanEndpointTests canPublishLaserScanWithEndpoint and canStartAndStopLaserScanReportingServiceWithEndpoint. Seriously now, how cool is that?

The first five parts of this series conclude the primary elements of developing well-designed packages for Robot Operating System (ROS) using proven design patterns and proper separation of concerns. Obviously, this is not a trivially simple approach to developing ROS packages; indeed, it would be overkill for very simple packages. But as packages grow in size, scope, and complexity, techniques described in this series should help to establish a maintainable, extensible package which doesn’t get too unruly as it evolves. In Part VI, the final part in this series, we’ll look at adding a simple UI layer, using wxWidgets, to interact with the package functionality.

Enjoy!
Billy McCafferty

Download the source for this post.