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.
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
ros2 pkg create
command means:
--build-type ament_python
[necessary] -- Specifies that this is a Python package using the ament build system (ROS2's build system)--license Apache-2.0
[optional] -- Sets the license for the package to Apache 2.0, which is a popular open-source licensefirst_ros2_nodes
[necessary] -- The name of the package you're creating--dependencies rclpy geometry_msgs std_msgs
[optional] -- Specifies the packages this new package depends on, if you don't specify them when creating the package, you can always add them later to the package.xml
):
rclpy
: The ROS2 Python client librarygeometry_msgs
: Contains message types for geometric data (like PointStamped)std_msgs
: Contains standard message types (like String, Int32, etc.)
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
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.
--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.
package.xml
and setup.py
files with the description of the package, maintainer information, license, and any dependencies:
package.xml
:
setup.py
:
description
- description of the ROS package in 1 sentencemaintainer
- your namemaintainer_email
- your email addressentry_points
- we'll leave this empty for now, but we'll be coming back to this later
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()
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
).
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.
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()
Much of this code is similar to our point_location_sender.py
code, so I'll highlight the key differences and new components:
'receive_point_location'
, which is unique from 'send_point_location'
of the other node we created.PointStamped
), the topic name ('/my_point'
), the callback function (self.listener_callback
) that will be called when a new message is received, and the queue size (10
).listener_callback
function is where we define what to do when a new message is received. In this case, we're simply logging the received point's coordinates to the console.rclpy.spin(node)
, until we shut it down, so we can wait around and log any PointStamped
messages on the /my_point
topic that we receive.
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.
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.