Introduction to ROS2: Sending and Receiving your First ROS2 Messages


Objectives


This exercise is designed to give you an introduction to sending and receiving ROS2 messages. If you run into any challenges, please reach out to the teaching team for help.

Create a new ROS2 package


Let's navigate to our directory within our intro_robo_ws for all of our in-class exercises and create a ROS2 package for today's coding exercises (to learn more about ROS2 package creation visit this ROS2 tutorial page). To do so, run the following commands in a terminal:

$ cd ~/intro_robo_ws/src
$ ros2 pkg create --build-type ament_python --license Apache-2.0 first_ros2_nodes --dependencies rclpy geometry_msgs std_msgs
Note: Let's break down what each argument in the ros2 pkg create command means:

Then be sure to build your workspace and source the install/setup.bash file.

$ cd ~/intro_robo_ws
$ colcon build --symlink-install --packages-select first_ros2_nodes
$ source install/local_setup.bash
Note: Every time you create a new package, change the package dependencies or anything in the package.xml or setup.py files, or create a new message type within a package, you MUST re-build the workspace and source the setup file in order for your changes to take effect.
Handy tip: By using the --symlink-install tag on the build, you will NOT need to re-build every time you edit your python scripts, which turns out to be pretty handy.
Every time you create a new package, be sure to edit the package.xml and setup.py files with the description of the package, maintainer information, license, and any dependencies:

Create a ROS2 node that publishes ROS2 messages


We're going to write Python code to construct a ROS2 node that publishes the location of a point in space (with x, y, and z coordinates) twice per second. Create a new python file named point_location_sender.py and ensure that it's full path is ~/intro_robo_ws/src/first_ros2_nodes/first_ros2_nodes/point_location_sender.py. To get started, put the following lines in your newly created point_location_sender.py:

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PointStamped

class PointLocationSender(Node):
    def __init__(self):
        super().__init__('send_point_location')
        self.publisher_ = self.create_publisher(PointStamped, '/my_point', 10)
        self.timer = self.create_timer(0.5, self.timer_callback)  # 2 Hz

    def timer_callback(self):
        msg = PointStamped()
        msg.header.stamp = self.get_clock().now().to_msg()
        msg.header.frame_id = "odom"
        msg.point.x = 1.0
        msg.point.y = 2.0
        msg.point.z = 0.0
        self.publisher_.publish(msg)
        self.get_logger().info(f'Publishing: [{msg.point.x}, {msg.point.y}, {msg.point.z}]')

def main(args=None):
    rclpy.init(args=args)
    node = PointLocationSender()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Understanding the Code


Let's break down each component of the code to better understand what's going on. Let's first take a look at the imports:

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PointStamped

You'll need import rclpy and from rclpy.node import Node for each Python node you'll create. Then, for each message type you need, you'll need to import it. Since we're sending 3D point locations, we need to import PointStamped from the geometry_msgs.msg package.

Next, let's look at the main function:

def main(args=None):
    rclpy.init(args=args) # Initializes the ROS2 Python client library
    node = PointLocationSender() # Creates an instance of our custom node class
    rclpy.spin(node) # Keeps the node running 
    node.destroy_node() # Cleans up the node
    rclpy.shutdown() # Shuts down the ROS2 Python client library

The main function handles ROS2 initialization, node creation, and cleanup (see the comments in the code above for more details). Importantly, this code uses rclpy.spin(), which enables the node to run indefinitely until you Ctrl+C (kill) it. This is helpful for this code example, however, you may not always want to use it, for example, if you want your code to run from start to finish and then exit itself.

Next, let's look at the __init__ method:

def __init__(self):
    super().__init__('send_point_location') # Initializes ROS2 node with unique name
    self.publisher_ = self.create_publisher(PointStamped, '/my_point', 10) # sets up ROS2 publisher
    self.timer = self.create_timer(0.5, self.timer_callback)  # Calls timer_callback every 0.5 seconds (2 Hz)

Importantly, this part of the code initializes the ROS2 node with a unique name (no ROS2 nodes can exist with the same name on the same network). It also sets up the publisher that specifies the message type (PointStamped), on a specified topic name (/my_point), with a specified queue size (10). The queue size determines how many messages to store if they can't be sent immediately. Finally, we set up a timer that will call the timer_callback method every 0.5 seconds (2 Hz).

Finally, our last portion of the code, the timer_callback method, creates and publishes our PointStamped message:

def timer_callback(self):
    msg = PointStamped() # creates a new ROS2 message of type PointStamped
    msg.header.stamp = self.get_clock().now().to_msg()
    msg.header.frame_id = "odom"
    msg.point.x = 1.0
    msg.point.y = 2.0
    msg.point.z = 0.0
    self.publisher_.publish(msg) # publishes the message
    self.get_logger().info(f'Publishing: [{msg.point.x}, {msg.point.y}, {msg.point.z}]') # log to the console

This code creates a new PointStamped message, fills it with data, and publishes it to the /my_point topic at a regular interval (2 Hz). To find out what format each kind of ROS2 message takes, you can either look at the documentation online (I found this by just Googling "PointStamped ROS2 message") or using the ROS2 command line tools (e.g., ros2 topic info /my_point and ros2 interface show /geometry_msgs/msg/PointStamped).

Running Your Code


Now that you've created and understand point_location_sender.py, you'll need to create an entry_point for this script in setup.py. Open up setup.py in the first_ros2_nodes package directory and add a line for your script to console_scripts, so your entry_points look like:

   entry_points={
       'console_scripts': [
           'sender = first_ros2_nodes.point_location_sender:main',
       ],
   },

Since we've modified setup.py, we need to re-build our package for the changes to take effect. Run:

$ cd ~/intro_robo_ws
$ colcon build --symlink-install --packages-select first_ros2_nodes
$ source install/setup.bash

Now, you can run your code:

$ ros2 run first_ros2_nodes sender

To check that your ROS2 node is really sending PointStamped messages, open another terminal and run:

$ ros2 topic echo /my_point

You should see your PointStamped messages printed to the console at a rate of 2 Hz.


Create a ROS2 node that subscribes to ROS2 messages using callback functions


In addition to publishing ROS2 messages, it's equally important to understand how ROS2 nodes can "listen to" or subscribe to ROS2 messages that are published by other nodes. This is accomplished with ROS2 callback functions. We'll now write a second Python script to construct a ROS2 node that subscribes to our PointStamped messages on the /my_point ROS2 topic and prints them out in the terminal.

Create a new Python script named point_location_receiver.py in the directory ~/intro_robo_ws/src/first_ros2_nodes/first_ros2_nodes. In this file, put the following code:

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PointStamped

class PointLocationReceiver(Node):
    def __init__(self):
        super().__init__('receive_point_location')
        self.subscription = self.create_subscription(
            PointStamped,
            '/my_point',
            self.listener_callback,
            10)

    def listener_callback(self, msg):
        self.get_logger().info(f'Received: [{msg.point.x:.2f}, {msg.point.y:.2f}, {msg.point.z:.2f}]')

def main(args=None):
    rclpy.init(args=args)
    node = PointLocationReceiver()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Understanding the Code


Much of this code is similar to our point_location_sender.py code, so I'll highlight the key differences and new components:

Running Your Code


Just as before with the sender, you'll need to create an entry_point for this script in setup.py. Open up setup.py in the first_ros2_nodes package directory and add a line for your script to console_scripts, so your entry_points look like:

   entry_points={
       'console_scripts': [
           'sender = first_ros2_nodes.point_location_sender:main',
           'receiver = first_ros2_nodes.point_location_receiver:main',
       ],
   },

Since we've modified setup.py, we need to re-build our package for the changes to take effect. Run:

$ cd ~/intro_robo_ws
$ colcon build --symlink-install --packages-select first_ros2_nodes
$ source install/setup.bash

Now, you can run your code:

$ ros2 run first_ros2_nodes receiver

With your sender running in one terminal and your receiver running in the other terminal, you should now be able to see the PointStamped messages being published by the sender and received by the receiver printed out in the terminal.


Acknowledgments


This coding exercise was based on the exercise presented in A Computational Introduction to Robotics course's Day 02 page and the Creating a package and Writing a simple publisher and subscriber (Python) ROS2 Humble tutorials.