#
Tutorial 1
#
Your first publisher and subscriber
Terminology note: A "node" refers to one of these publisher or subscriber units -- you can think of them as just a program that does "something" within ROS. A "topic" refers to the place a publisher publishes its messages for subscribers to read. For example, a camera might publish images to /image
-- then any subscriber can listen to /image
and pick up any image from the camera. Anything sent by a publisher node to its respective topic is called a "message"
#
Prerequisites
All that follows is assuming you have successfully completed
and you have a working docker container. If you haven't done so, take the time to do so now. If you have hit a roadblock in the setup, ask a lead for assistance (or google it and try to figure it out on your own...)
#
Step 0: Create the package
In order to write our nodes, we need to first create its package. This is will simply give us the scaffolding for a project, so that we have all the resources we need.
If you haven't already, go ahead and launch your docker and make sure you're attached to it (you should see that big yellow [DOCKER]
on your command prompt). Go ahead and navigate to your src
directory (cd src
). Then, to create the package, simply run
ros2 pkg create --build-type ament_python py_pubsub
Where ros2
is the executable we're running, pkg
tells ros2
we want to do something with a package, create
tells it we want to create one, --build-type ament_python
tells the package creation tool that we want a pure python package, and py_pubsub
is the name of the package.
Now, navigate to the source directory of your new package at py_pubsub/py_pubsub/
, and create/open a new file called publisher_member_function.py
.
Note: Files for this and subsequent projects can be edited in a number of ways. Any IDE can edit these files as the docker environment has src/
outside the docker being the same as src/
inside the docker -- just navigate to where you have this tutorial and open the file you want to edit!
We'll now write the node step by step. You can either read these sections step by step and then copy based off the full file shown later, or you can copy with each step by step.
Tip: type the code yourself even if you're typing exactly what's written here -- it will help you be familiar with what you've written.
#
Step 0.5: What are we building?
We are going to build a simple publisher node and subscriber node. The publisher node will publish some message to 'topic' on some repeated timed interval, and the subscriber node will read these messages and print them out for us.
#
Step 1: Writing the publisher
Let's get coding!
We'll start by importing the necessary libraries: rclpy
(ROS Client Library for Python) and the object Node
from rclpy.node
:
import rclpy
from rclpy.node import Node
Next, we need to make sure we have the object we want to be able to send with our publisher. So, we'll go ahead and import the standard "String" message since all we want to send is text:
from std_msgs.msg import String
Now we begin the body of the code, which we'll start by creating our publisher node's python class. Python's inheritance is out of the scope of this tutorial, and isn't needed to understand what's going on, but for those who want to take the time to learn it can look at a good inheritance explainer by GeeksForGeeks:
You'll seeNode
in the parentheses of the class definition because we want our publisher node to be built on top of the Node
class:
class MinimalPublisher(Node):
All python classes require a constructor to be defined via __init__(self)
, and this node is no different.
We'll start by calling the constructor of the class we're inheriting from and giving our node a name via super().__init__('minimal_publisher')
.
Next, we'll initialize a publisher object and make it a class member by adding self.publisher = self.create_publisher(String, 'topic', 10)
. This makes self.publisher
our node's publisher object (which we'll call to publish) by calling the inherited class Node
's function create_publisher(msg_type, topic_name, queue_size)
(the queue_size
parameter denotes the allowed amount of queued messages if a subscriber isn't receiving them fast enough).
Finally, we'll define our timer interval, create the timer object, and initialize a member index counter to 0.
All together, we get
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5
# self.timer_callback refers to a function we'll define later that we want the timer
# object to call every time the interval completes
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
Now we want to define timer_callback()
so that the timer has something to do. Here we'll create a new message by initializing a String()
object, we'll define the data part of the message, then we'll call self.publisher_
to publish our message, then finishing the function by logging that we've published a message and incrementing our counter variable:
def timer_callback(self):
msg = String()
msg.data = f"Hello world: {self.i}"
self.publisher_.publish(msg)
self.get_logger().info(f"Publishing: \"{msg.data}\"")
self.i += 1
Finally, we'll create the main
function for the script, so that python knows what to do when ROS runs our package. Unlike the previous two functions, this is not supposed to be a part of the MinimalPublisher
node.
Here we'll initialize rclpy
, initialize our publisher node, "spin" up the node (start it), and then tell it to destroy and shutdown once it's done:
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
# We won't reach this point until the publisher has been told to stop running
minimal_publisher.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
Just like that, our publisher is done! All together, this script should look like this:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = f"Hello world: {self.i}"
self.publisher_.publish(msg)
self.get_logger().info(f"Publishing: \"{msg.data}\"")
self.i += 1
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
minimal_publisher.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
#
Step 2: Writing the subscriber
In the same place as the previous file, now create the file subscriber_member_function.py
. Go ahead and open it and let's start writing!
This node is almost the exact same as the publisher, with a few select differences. Rather than step by step through the entire script again, we're just going to speak on the differences.
The imports are the exact same, but the class has a different name now -- MinimalSubscriber
. Thus, the first section is simply
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
Within the constructor, we'll want to change the name to 'minimal_subscriber', and we'll want to define a subscriber object rather than a publisher object, giving us:
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
String, # Message type
'topic', # topic name
self.listener_callback, # Function to call whenever we receive a message
10 # Queue size
)
self.subscription
Now completely different from the publisher is the callback function. We don't care about any timers or publishers, we just want to print out our message whenever we get it. The callback you put into the self.create_subscription
function will have one parameter (aside from self
as it is a member function), that parameter being the received message. Thus, our listener callback should look like the following:
def listener_callback(self, msg):
self.get_logger().info(f"Recieved msg: \"{msg.data}\"")
Finally, the main function and body are the exact same, just changing names to reflect the fact that this is the subscriber and not the publisher.
The full script should look like this:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10
)
self.subscription
def listener_callback(self, msg):
self.get_logger().info(f"Recieved msg: \"{msg.data}\"")
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
minimal_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
Now that we have both the publisher and subscriber, we need to configure the package to make sure we have all the required dependencies to run it.
#
Step 3: Configuring the package
Contained one folder back from where we stored our source code just now are a couple important files, setup.py
, setup.cfg
, and package.xml
.
setup.py
tells Python how your project is structured and what files to run if the project is asked to execute some code, setup.cfg
are various ROS building arguments, and package.xml
contains important information about the package as a whole, like the project's dependencies. We have some dependencies to add, so go ahead and open package.xml
for editing.
Underneath some metainfo definitions like <license>
, you should see a handful of lines which read along the lines of <test_depend> (something) </test_depend>
. Above those statements and below the metainfo, we want to add our two dependencies:
<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>
Next, open up setup.py
so that we can add "entrypoints" -- essentially telling python what code to run if it's asked to run "talker" or "listener". Add the following to "entry_points":
entry_points={
'console_scripts': [
'talker = py_pubsub.publisher_member_function:main',
'listener = py_pubsub.subscriber_member_function:main',
]},
#
Step 4: Building & Running
To start, navigate to the root of the workspace (/ros2_ws/
).
The docker image should automatically do the following step, but it's good to do yourself anyway for redundancy. We'll just make sure we have all of our ROS dependencies by running
rosdep install -i --from-path src --rosdistro iron -y
Next (still in the workspace root), we'll build the project via
colcon build --packages-select py_pubsub
and then
source install/setup.bash
Important
To be able to run both nodes at the same time and see each of their outputs, we'll need to open another terminal. To do this, we'll use tmux
(terminal multi-plexer) which is just a way to split one terminal into multiple other "panes" of terminals. To start simply type tmux
in the terminal and run it. You should've opened into a new terminal and there should be a green bar on the bottom of the terminal. To split the view and add another pane, use the key chord Ctrl+b, "
(Ctrl+b then quotation marks (shift+')) for a horizontal pane or Ctrl+b, %
for a vertical pane. To switch between these panes you can just use the key chord Ctrl+b, <arrow direction>
depending on which direction you want to go.
Now with tmux up and running, in one pane go ahead and run
ros2 run py_pubsub talker
to start the publisher. On that pane, you should start seeing some output like
[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
...
Next, move over to your other pane and run ros2 run py_pubsub listener
to start the subscriber. Once this node is up and running, it should begin outputting the messages it's receiving from the publisher at that moment. For example:
[INFO] [minimal_subscriber]: Received msg:: "Hello World: 20"
[INFO] [minimal_subscriber]: Received msg:: "Hello World: 21"
[INFO] [minimal_subscriber]: Received msg:: "Hello World: 22"
[INFO] [minimal_subscriber]: Received msg:: "Hello World: 23"
[INFO] [minimal_subscriber]: Received msg:: "Hello World: 24"
Congrats, your first publisher and subscriber nodes are up and running! To turn them off just Ctrl+c
in their respective panes. You can exit tmux by either running exit
within each pane or using Ctrl+b, x
to close each pane (it will prompt asking if you want to close the pane, to which you will confirm by just pressing 'y').