Skip to content

Lab 5.1. ROS Topics in Python

Estimated time to complete this lab: 1 hour

Objectives

At the end of this self-learning lab, you should be able to:

  • Write a publisher and subscriber node in Python

Preparations

1. Download the tutorial package lab5 from Github and place it in your ROS workspace

  • You should fork the tutorial repository (https://github.com/m2robocon/m2cs_ros_tutorial) to your personal Github acount.
  • Clone the forked tutorial repository to your workspace. The path of the repository should look something like ~/catkin_ws/src/m2cs_ros_tutorial.

2. Allow ROS to recognize the new package

  • When in your workspace directory (pwd is catkin_ws), run catkin_make

    • The meaning of catkin_make will be explained in 05 L3.
    • If you run into errors about the C/C++ compiler not being installed, you need to install g++ on your computer.
      To install the g++ compiler, run sudo apt-get install g++
  • Verify that ROS recognizes the new package: run rospack list | grep lab5

rospack list | grep lab5 lab5 /home/m2/catkin_ws/src/m2cs_ros_tutorial/lab5
  • If you do not see the package path listed, then probably you need to source the setup file of your workspace: Run the following command:
$ echo "source /home/m2/catkin_ws/devel/setup.bash" >> ~/.bashrc && source ~/.bashrc

Section 0. Node

  • A node is an executable.
  • Nodes are placed inside packages. For example, lab5 is a package.
  • In Python, ROS is just a library that can be imported inside Python. The programming concepts that you learnt in other courses can still be used in ROS.
  • A python script can be modified to become a ROS node by adding the relevant library codes provided by ROS.

Basic code structure for a ROS node

  • Look at the content of lab5/src/node_basic.py.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/bin/env python3
import rospy

if __name__ == '__main__':
    rospy.init_node('node_basic')

    rate = rospy.Rate(2)
    while not rospy.is_shutdown():
        rospy.loginfo("hello, world!")
        rate.sleep()

Explanations

Let's have a look at what the code is doing, line by line:

#!/usr/bin/env python3
Make sure your script is executed as a Python script with python3.

Note

The #!/usr/bin/env python line points to the path that the command python3 is installed.

import rospy
You need to import rospy if you are writing a ROS Node.
rospy.init_node('node_basic')
Tells rospy that this executable is a node, with the name node_basic
rate = rospy.Rate(2)

The Rate object allows us to use its method sleep() to loop at a desired rate.

By calling sleep() inside a loop, the program sleeps for a period of time such that the desired looping rate is maintained.

while not rospy.is_shutdown():
To create a forever loop in ROS, use while not rospy.is_shutdown(): condition instead of while True: to allow more smooth node killing without SIGINT.
rospy.loginfo("hello, world!")
loginfo prints the text to screen, which is also logged inside the node's log file.
rate.sleep()
Sleep for a certain amount of time to maintain the loop rate defined in rate = rospy.Rate(2).

Running the node

  • Run roscore. This should be running as long as you are running ROS. Don't kill it!
roscore ... logging to /home/m2/.ros/log/4749b7b8-e3b1-11ea-aa8c-1c1b0d98dfee/roslaunch-tux-9534.log
Checking log directory for disk usage. This may take awhile.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://m2-null:46785/
ros_comm version 1.12.14


SUMMARY

PARAMETERS
* /rosdistro: kinetic
* /rosversion: 1.12.14

NODES

auto-starting new master
process[master]: started with pid [9544]
ROS_MASTER_URI=http://m2-null:11311/

setting /run_id to 4749b7b8-e3b1-11ea-aa8c-1c1b0d98dfee

process[rosout-1]: started with pid [9557] started core service [/rosout]

Note

Recall that rospy.loginfo() prints text to screen and also writes to a log file.

The directory of the log file is stated in the roscore output, under ... logging to /home/m2/.ros/log/4749b7b8-e3b1-11ea-aa8c-1c1b0d98dfee/roslaunch-tux-9534.log.

  • To run a node in the command line, use rosrun [package] [node]. Try to run the basic node on a separate terminal : rosrun lab5 node_basic.py You should see errors about permissions like the following:
$ rosrun lab5 node_basic.py
[rosrun] Couldn't find executable named node_basic.py below /home/m2/catkin_ws/src/m2cs_ros_tutorial/lab5
[rosrun] Found the following, but they're either not files,
[rosrun] or not executable:
[rosrun]   /home/m2/catkin_ws/src/m2cs_ros_tutorial/lab5/src/node_basic.py

Bug

  • Oops! Seems that node_basic.py is not an executable. Let's fix that by adding the executable permission:
    • Change directory to the package: $ roscd lab5/src
    • Add the executable permission: $ chmod +x node_basic.py
  • Now that the execution permission has been added, we can run the node again, this time without errors:
rosrun lab5 node_basic.py [INFO] [1562942112.307432]: hello, world! [INFO] [1562942112.807662]: hello, world! [INFO] [1562942113.307636]: hello, world! ... (omitted)
  • You should see that the text of loginfo("hello, world!") is printed at a rate of 2 messages per second (also note that the timestamps are 0.5s apart). This rate is from the rospy.Rate(2) object.

  • Verify the node is running by listing out currently running nodes with rosnode list:

$ rosnode list
/rosout
/node_basic
  • /node_basic is our executable, with the name from the line rospy.init_node('node_basic').

Tip

Note that /node_basic under rosnode list is not extracted from the filename node_basic.py.

Instead, is extracted from the node name, which was defined using rospy.init_node('node_basic'). In other words, the node name (within ROS) and filename (within the Linux filesystem) do not necessarily have to be the same.

Note

You can see another node named /rosout.

rosout is the name of the console log reporting mechanism in ROS. It can be thought as comprising several components:

  • The rosout node for subscribing, logging, and republishing the messages.
  • The /rosout topic
  • The /rosout_agg topic for subscribing to an aggregated feed
  • And others. For more information, see the ROS documentation on rosout.

This ndoe is always running as it collects and logs nodes' debugging output (for example from loginfo).


Communication between nodes via ROS Topics: Basic concepts

Recommended reading: ROS Tutorial: Understanding Topics

For a more visual explanation, refer to the slides of this chapter: 05 ROS Node

  • To say that two programs are communicating with each other, it means that the output of one program is the input of another.
  • In ROS, one of the ways for nodes to communicate with each other is via a topic.
  • A topic is a named data bus that contains data. A node can write data onto this topic, and another node can read data from such topic. The data transfer is facilitated via the topic.
  • Each data packet on the bus is called a message. Nodes can only read/write to a topic one message at a time.
  • Each message has its own format and structure (this will become apparent with an example later). This structure is called a msg. We will use "message type" and "msg" interchangeably.
  • To say that a node is to write (output) to a topic, with ROS terminology is to publish.
  • To say that a node is to read (input) from a topic, with ROS terminology is to subscribe.
  • Multiple nodes can write (publish) to the same topic, and multiple nodes can also read (subscribe) from the same topic.
  • The same node can publish to topics and subscribe to multiple topics simultaneously.

Publisher-Subscriber demo

  • On two separate terminals (remember to keep roscore running somewhere else!), run the publisher (talker.py) and subscriber (listener.py) node respectively:
  • $ rosrun lab5 talker.py
  • $ rosrun lab5 listener.py
  • You should see talker.py displaying some text, and listener.py receiving that text.

Failure

If you encounter execution permission errors when running the nodes, you should run chmod +x accordingly again. Refer to the previous section on how to use this!

rosrun lab5 talker.py [INFO] [1563203461.317358]: 1 [INFO] [1563203461.417657]: 2 [INFO] [1563203461.517766]: 3 ... (omitted)
rosrun lab5 listener.py [INFO] [1563203461.317358]: I heard 1 abc [INFO] [1563203461.417657]: I heard 2 abc [INFO] [1563203461.517766]: I heard 3 abc ... (omitted)

Explanations

The following will become apparent in the code, but we show the basic flow below:

  • The two nodes (talker, listener) communicate on the chatter topic.
  • Messages on the chatter topic are of the msg type Chat.
  • talker is a node that publishes Chat messages to the chatter topic.
  • listener is a node that subscribes Chat messages from the chatter topic.
  • In particular, a function in listener.py will be called (basically) every time there is a new message published to chatter, regardless of which node published the message.

Note

Acutally there are more advanced things going on behind the scenes for communication. However, they are not the focus of this beginner tutorial. Advanced topics such as ROS threads and queues will be discussed in later training.

(TODO: Insert explanation diagram)


Section 1. Publisher

  • To make a node able to write to a topic, code for a publisher is needed.

The code

  • Look at the content of lab5/src/talker.py.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3

import rospy
from lab5.msg import Chat

if __name__ == '__main__':
    rospy.init_node('talker') 
    pub = rospy.Publisher('chatter', Chat, queue_size = 1)

    rate = rospy.Rate(10) 
    cur = 1

    while not rospy.is_shutdown():
        c = Chat()
        c.id = cur; c.text = "abc"
        rospy.loginfo(cur) 

        pub.publish(c) 
        # pub.publish(cur, "abc")
        # pub.publish(text = "abc")

        rate.sleep()
        cur += 1
  • Look at the content of lab5/msg/Chat.msg.
int32 id
string text

Explanations

We just have to add a few more lines into the basic node structure to allow it to publish topics. Let's have a look at what new publisher code (highlighted) has been added:

from lab5.msg import Chat

lab5.msg import allows us to use the lab5/Chat message type for publishing.

  • Chat is a python class. The Chat class is generated from the file lab5/msg/Chat.msg. This will be discussed in detail in 05 L3 ROS Package msg srv.

  • (advanced) Acutally, the class was generated by ROS when you ran catkin_make when during the setup. The generated class is located at ~/catkin_ws/devel/lib/python2.7/dist-packages/lab5/msg/_Chat.py.

pub = rospy.Publisher('chatter', Chat, queue_size = 1)

This creates a publisher object in pub that publishes to the chatter topic which is of type Chat. This publisher object offers us a function publish() that we can use to publish messages later.

  • queue_size limits the amount of queued messages if any subscriber is not receiving them fast enough (in M2, we generally set it to 1).
c = Chat()
c.id = cur; c.text = "abc"

Creates a Chat message object in c and assigns values to the variable fields inside Chat. The message fields can be accessed via the . (dot) operator. Recall that the msg definition of Chat is stored in lab5/msg/Chat.msg.

pub.publish(c)

Publish the Chat object named c to the chatter topic.

  • pub.publish(cur, "abc"): You can also choose to not construct the Chat object and pass the message fields directly into the arguments of the publish() function. In general, the constructor arguments are in the same order as in the .msg file.
  • pub.publish(text = "abc"): You can also choose to only initialize some of the fields and leave the rest with default values (0).

Running the node

  • Let's run the publisher node we have just went through:
rosrun lab5 talker.py [INFO] [1563203461.317358]: 1 [INFO] [1563203461.417657]: 2 [INFO] [1563203461.517766]: 3 ... (omitted)
  • To see what topics are being published by a node, use rosnode info [node]:
rosnode info talker -----------------------------------------------
Node [/talker]
Publications:
* /chatter [lab5/Chat]
* /rosout [rosgraph_msgs/Log]

Subscriptions: None

... (omitted)
  • This tells us the /talker node is publishing to the /chatter topic.

  • To show the data being published on a topic, use rostopic echo [topic]

rostopic echo /chatter id: 1
text: "abc"
---
id: 2
text: "abc"
---
id: 3
text: "abc"
---
... (omitted)
  • The message data assigned by the line c.id = cur; c.text = "abc" has been published and received by the terminal! Thus, we can see that the topic has been published successfully!

Section 2. Subscriber

  • To make a node receive data from a topic, code for a subscriber is needed.
  • The subscriber receives the topic data via a callback function, which is invoked with the message data (a message object, which is a Chat object in our example) as an argument whenever something is published to that topic.

The code

  • Look at the content of lab5/src/talker.py.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/usr/bin/env python3

import rospy
from lab5.msg import Chat

def callback(data):
    rospy.loginfo('I heard %d %s', data.id, data.text)

if __name__ == '__main__':
    rospy.init_node('listener')
    rospy.Subscriber('chatter', Chat, callback)

    rospy.spin()

    print("node terminated")

Explanations

The code for listener.py is similar to talker.py, except that there is a new callback mechanism for subscribing to messages (those highlighted are new and subscriber-related).

def callback(data): ...
The callback function is called whenever a new message is published to the chatter topic. The message contents are stored in data, and the message fields can be accessed via the . (dot) operator.
rospy.Subscriber('chatter', Chat, callback)

This declares the node subscribes to the chatter topic which is of type Chat. When new messages are received, callback is invoked with the message as the first argument.

rospy.spin()

rospy.spin() is a function that when called, does not terminate until you shutdown the node in ROS (e.g. via Ctrl+C in bash). This can be used to prevent the node from exiting until the node has been shutdown.

Without this line, the node will terminate by itself by running to the end of main , and callback will not be invoked even if there are new publications on chatter.

Running the node

  • Remain the chatter publisher node running.
  • Let's run the listener subscriber node:
rosrun lab5 listener.py [INFO] [1562957324.036534]: I heard 1 abc [INFO] [1562957324.136728]: I heard 2 abc [INFO] [1562957324.236694]: I heard 3 abc ... (omitted)
  • The talker node has published message contents id and text, and that data has been subscribed and received by the listener node. These two nodes are already communicating with each other!
    The terminal output is from the rospy.loginfo('I heard %d %s', data.id, data.text) line.
  • If you kill the node with Ctrl+C, node terminated is printed. This is the behavior of rospy.spin() which prevents the program from exiting before the node is actually terminated by ROS.
  • Apart from testing the subscriber behavior with a publisher node, we can also publish topics from the command line using rostopic pub:
  • Leave listener running, and kill chatter.
  • In a new command line, type rostopic pub /chatter without pressing Enter.
  • Type Tab twice and the topic type lab5/Chat should appear.
  • Type Tab twice again, the message fields "id: 0 text: ''" should appear. Enter any integer to replace the 0 and type any text within the single quotes ''.
  • Type Enter to publish a new message to the /chatter topic.
rostopic pub /chatter lab5/Chat "id: 123 text: 'hi'" publishing and latching message. Press ctrl-C to terminate
  • On the terminal running the listener node, you should see that the node has received the message via the subscriber callback:
$ rosrun lab5 listener.py [INFO] [1562958088.171434]: I heard 123 hi

Checklist and Assignment

Completion

Congratulations! You have successfully run a publisher and subscriber and make two nodes communicate with each other!

Now, you should have enough knowledge to complete the first two tasks in Assignment 1 (m2_ps4 manual control).

  • Creating a node from a regular python script
  • Publishing to a topic (declare publisher object, publish() function)
  • Subscribing to a topic (declare subscriber, self-defined callback function)
  • Command line tools: rosrun, rosnode, rostopic

References