[ROS Q&A] 176 – Publisher/Subscriber tutorial – avoid losing some messages from the publisher

Written by Alberto Ezquerro

24/01/2019

Learn how to make sure that the Subscriber receives all messages sent by a C++ ROS Publisher to avoid losing any messages from the publisher. This post answers the following question found on the ROS Answers forum: https://answers.ros.org/question/313491/cpp-publishersubscriber-tutorial-loosing-some-messages/. Let’s go!

Related Resources

Step 1: Get your development environment ready

Either of the following will do:

  1. Use the ROS Development Studio (ROSDS), an online platform for developing for ROS within a PC browser. Easy-peasy. I’m using this option for this post
    1. Once you log in, click on the New ROSject button to create a project that will host your code. Then Click on Open ROSject to launch the development/simulation environment.
    2. To open a “terminal” on ROSDSpick the Shell app from the Tools menu.
    3. You can find the IDE app on the Tools menu.
  2. You have ROS installed on a local PC. Okay, skip to Step 2.

Next step!

Step 2: Reproduce the problem – let’s lose some messages from the publisher!

Create the talker and listener nodes as specified in the tutorial: http://wiki.ros.org/ROS/Tutorials/WritingPublisherSubscriber(c%2B%2B)

1. If you are using ROSDS, start by creating a and opening a ROSject as described above.

2. Open a terminal and create a package:

user:~$ cd catkin_ws/src
user:~/catkin_ws/src$ catkin_create_pkg talker_listener roscpp
Created file talker_listener/CMakeLists.txt
Created file talker_listener/package.xml
Created folder talker_listener/include/talker_listener
Created folder talker_listener/src
Successfully created files in /home/user/catkin_ws/src/talker_listener. Please adjust the values in package.xml.
user:~/catkin_ws/src$

3. Create the talker and listener source files

user:~/catkin_ws/src$ cd talker_listener/src
user:~/catkin_ws/src/talker_listener/src$ touch talker.cpp listener.cpp

4. Open the talker.cpp and listener.cpp files in the IDE and paste in the following code

talker.cpp

#include "ros/ros.h"
#include "std_msgs/String.h"

#include <sstream>

/**
 * This tutorial demonstrates simple sending of messages over the ROS system.
 */
int main(int argc, char **argv)
{
  /**
   * The ros::init() function needs to see argc and argv so that it can perform
   * any ROS arguments and name remapping that were provided at the command line.
   * For programmatic remappings you can use a different version of init() which takes
   * remappings directly, but for most command-line programs, passing argc and argv is
   * the easiest way to do it.  The third argument to init() is the name of the node.
   *
   * You must call one of the versions of ros::init() before using any other
   * part of the ROS system.
   */
  ros::init(argc, argv, "talker");

  /**
   * NodeHandle is the main access point to communications with the ROS system.
   * The first NodeHandle constructed will fully initialize this node, and the last
   * NodeHandle destructed will close down the node.
   */
  ros::NodeHandle n;

  /**
   * The advertise() function is how you tell ROS that you want to
   * publish on a given topic name. This invokes a call to the ROS
   * master node, which keeps a registry of who is publishing and who
   * is subscribing. After this advertise() call is made, the master
   * node will notify anyone who is trying to subscribe to this topic name,
   * and they will in turn negotiate a peer-to-peer connection with this
   * node.  advertise() returns a Publisher object which allows you to
   * publish messages on that topic through a call to publish().  Once
   * all copies of the returned Publisher object are destroyed, the topic
   * will be automatically unadvertised.
   *
   * The second parameter to advertise() is the size of the message queue
   * used for publishing messages.  If messages are published more quickly
   * than we can send them, the number here specifies how many messages to
   * buffer up before throwing some away.
   */
  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);

  ros::Rate loop_rate(10);

  /**
   * A count of how many messages we have sent. This is used to create
   * a unique string for each message.
   */
  int count = 0;
  while (ros::ok())
  {
    /**
     * This is a message object. You stuff it with data, and then publish it.
     */
    std_msgs::String msg;

    std::stringstream ss;
    ss << "hello world " << count;
    msg.data = ss.str();

    ROS_INFO("%s", msg.data.c_str());

    /**
     * The publish() function is how you send messages. The parameter
     * is the message object. The type of this object must agree with the type
     * given as a template parameter to the advertise<>() call, as was done
     * in the constructor above.
     */
    chatter_pub.publish(msg);

    ros::spinOnce();

    loop_rate.sleep();
    ++count;
  }


  return 0;
}

listener.cpp

#include "ros/ros.h"
#include "std_msgs/String.h"

/**
 * This tutorial demonstrates simple receipt of messages over the ROS system.
 */
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
  ROS_INFO("I heard: [%s]", msg->data.c_str());
}

int main(int argc, char **argv)
{
  /**
   * The ros::init() function needs to see argc and argv so that it can perform
   * any ROS arguments and name remapping that were provided at the command line.
   * For programmatic remappings you can use a different version of init() which takes
   * remappings directly, but for most command-line programs, passing argc and argv is
   * the easiest way to do it.  The third argument to init() is the name of the node.
   *
   * You must call one of the versions of ros::init() before using any other
   * part of the ROS system.
   */
  ros::init(argc, argv, "listener");

  /**
   * NodeHandle is the main access point to communications with the ROS system.
   * The first NodeHandle constructed will fully initialize this node, and the last
   * NodeHandle destructed will close down the node.
   */
  ros::NodeHandle n;

  /**
   * The subscribe() call is how you tell ROS that you want to receive messages
   * on a given topic.  This invokes a call to the ROS
   * master node, which keeps a registry of who is publishing and who
   * is subscribing.  Messages are passed to a callback function, here
   * called chatterCallback.  subscribe() returns a Subscriber object that you
   * must hold on to until you want to unsubscribe.  When all copies of the Subscriber
   * object go out of scope, this callback will automatically be unsubscribed from
   * this topic.
   *
   * The second parameter to the subscribe() function is the size of the message
   * queue.  If messages are arriving faster than they are being processed, this
   * is the number of messages that will be buffered up before beginning to throw
   * away the oldest ones.
   */
  ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);

  /**
   * ros::spin() will enter a loop, pumping callbacks.  With this version, all
   * callbacks will be called from within this thread (the main one).  ros::spin()
   * will exit when Ctrl-C is pressed, or the node is shutdown by the master.
   */
  ros::spin();

  return 0;
}

5. Add the following lines to the build section of the CMakeList.txt file, so the files can build:

add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
add_dependencies(talker talker_listener_generate_messages_cpp)

add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
add_dependencies(listener talker_listener_generate_messages_cpp)

6. Build the package and source the workspace:

user:~/catkin_ws$ catkin_make
user:~/catkin_ws$ source devel/setup.bash

7. Start the listener node first in the current terminal and then start the talker node in another terminal.

Start the listener first. You should not see any message yet.

user:~/catkin_ws$ rosrun talker_listener listener

(PS: If you get an error that master cannot be reached, run nohup roscore & in the current terminal to start the ROS master)

Start the talker in a new terminal.

user:~$ cd catkin_ws
user:~/catkin_ws$ source devel/setup.bash
user:~/catkin_ws$ rosrun talker_listener talker

You would notice that even though the talker publishes the messages starting from zero, the listener only gets the messages starting from 3 (or even 4):

# Talker

user:~/catkin_ws$ rosrun talker_listener talker
[ INFO] [1586183868.773366254]: hello world 0
[ INFO] [1586183868.873393538]: hello world 1
[ INFO] [1586183868.973385317]: hello world 2
[ INFO] [1586183869.073401909]: hello world 3
[ INFO] [1586183869.173393278]: hello world 4
[ INFO] [1586183869.273392714]: hello world 5

#### Listener

user:~/catkin_ws$ rosrun talker_listener listener
[ INFO] [1586183869.073691834]: I heard: [hello world 3]
[ INFO] [1586183869.173605854]: I heard: [hello world 4]
[ INFO] [1586183869.273767103]: I heard: [hello world 5]

So the problem is real! Let’s fix it in the next section!

Step 2: Fix the problem: avoid losing messages from the publisher

What’s the point of talking when no one is listening? We need to make the talker wait for the listener before publishing the message by adding the following lines to the talker.cpp file, before the publish command:

while(chatter_pub.getNumSubscribers() == 0)
    loop_rate.sleep();

So your talker.cpp should now be:

#include "ros/ros.h"
#include "std_msgs/String.h"

#include <sstream>

/**
 * This tutorial demonstrates simple sending of messages over the ROS system.
 */
int main(int argc, char **argv) {
  /**
   * The ros::init() function needs to see argc and argv so that it can perform
   * any ROS arguments and name remapping that were provided at the command
   * line. For programmatic remappings you can use a different version of init()
   * which takes remappings directly, but for most command-line programs,
   * passing argc and argv is the easiest way to do it.  The third argument to
   * init() is the name of the node.
   *
   * You must call one of the versions of ros::init() before using any other
   * part of the ROS system.
   */
  ros::init(argc, argv, "talker");

  /**
   * NodeHandle is the main access point to communications with the ROS system.
   * The first NodeHandle constructed will fully initialize this node, and the
   * last NodeHandle destructed will close down the node.
   */
  ros::NodeHandle n;

  /**
   * The advertise() function is how you tell ROS that you want to
   * publish on a given topic name. This invokes a call to the ROS
   * master node, which keeps a registry of who is publishing and who
   * is subscribing. After this advertise() call is made, the master
   * node will notify anyone who is trying to subscribe to this topic name,
   * and they will in turn negotiate a peer-to-peer connection with this
   * node.  advertise() returns a Publisher object which allows you to
   * publish messages on that topic through a call to publish().  Once
   * all copies of the returned Publisher object are destroyed, the topic
   * will be automatically unadvertised.
   *
   * The second parameter to advertise() is the size of the message queue
   * used for publishing messages.  If messages are published more quickly
   * than we can send them, the number here specifies how many messages to
   * buffer up before throwing some away.
   */
  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);

  ros::Rate loop_rate(10);

  /**
   * A count of how many messages we have sent. This is used to create
   * a unique string for each message.
   */
  int count = 0;
  while (ros::ok()) {
    /**
     * This is a message object. You stuff it with data, and then publish it.
     */
    std_msgs::String msg;

    std::stringstream ss;
    ss << "hello world " << count;
    msg.data = ss.str();

    ROS_INFO("%s", msg.data.c_str());

    /**
     * The publish() function is how you send messages. The parameter
     * is the message object. The type of this object must agree with the type
     * given as a template parameter to the advertise<>() call, as was done
     * in the constructor above.
     */
    while (chatter_pub.getNumSubscribers() == 0)
      loop_rate.sleep();

    chatter_pub.publish(msg);

    ros::spinOnce();

    loop_rate.sleep();
    ++count;
  }

  return 0;
}

Build the package again, from the first terminal:

user:~/catkin_ws$ catkin_make

Start the listener:

user:~/catkin_ws$ source devel/setup.bash
user:~/catkin_ws$ rosrun talker_listener listener

Then start the talker:

user:~/catkin_ws$ source devel/setup.bash
user:~/catkin_ws$ rosrun talker_listener talker

Now you should see that the messages are in sync:

# Talker

user:~/catkin_ws$ rosrun talker_listener talker
[ INFO] [1586183868.773366254]: hello world 0
[ INFO] [1586183868.873393538]: hello world 1
[ INFO] [1586183868.973385317]: hello world 2

#### Listener

user:~/catkin_ws$ rosrun talker_listener listener
[ INFO] [1586183869.073691834]: I heard: [hello world 0]
[ INFO] [1586183869.173605854]: I heard: [hello world 1]
[ INFO] [1586183869.273767103]: I heard: [hello world 2]

Yes, we did it! See you next time.

Extra: Video of the post

Here below you have a “sights and sounds” version of this post, just in case you prefer it that way. Enjoy!

Feedback

Did you like this post? Do you have any questions about the explanations? Whatever the case, please leave a comment on the comments section below, so we can interact and learn from each other.

If you want to learn about other ROS or ROS2 topics, please let us know in the comments area and we will do a video or post about it.


Edited by Bayode Aderinola

Topics: ROS Q&A

Check Out These Related Posts

1 Comment

  1. Carsten

    Thank you!

    Reply

Submit a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Pin It on Pinterest

Share This