Today
For Next Time
Abstract
As we go into the final project, you will want to add a few more debugging tools to your quiver. Today, I have created some self-guided exercises to help you get to know these new approaches.
Python Debugger and ROS
If you've never used the Python debugger, you really should try it out! Among other things, the Python debugger let's you stop the execution of a running Python script so that you can query the state of its variables and optionally set the next line of code that you want to stop at for additional inspection. This functionality is particularly nice when debugging ROS nodes as an alternative to print statement debugging which can be cumbersome for callback functions that are executed multiple times a second.
The most popular debugger is called PDB. There are many tutorials that you can easily find on the internet (here is a pretty basic one). The main documentation can be found here.
To run the Python debugger with ROS there are two options. The first is that you can create a launch file and use the launch-prefix attribute (see this tutorial, or check the launch subdirectory of the comprobo15/python_debugging_practice
). Otherwise, you can simply directly execute your Python script with the -m pdb option.
python -m pdb path/to/your/python_script.py
When it comes to debugging ROS nodes, PDB comes with a few caveats. The most significant one is that when setting a break point in PDB (a break point is where you would like the program execution to pause so you can inspect the program state) the line you set as the break will only be respected if you reach that line in the same thread as you set the break point in. This is an issue in ROS since callbacks are executed on different threads. To see this, run this command:
python -m pdb ~/catkin_ws/src/comprobo15/python_debugging_practice/scripts/simple_node.py
Once you have done this, set a breakpoint at line 20 (which is this line of code: r.sleep()
), and then run the ROS node. In the Python debugger, execute these commands:
b 20
r
As expected, your program should stop execution at line 20. At this point you can print out some program state (not that there is much), or continue to the next break point by running the c
command.
Next, quit your ROS node and start it up again using the python -m pdb
command above. Instead of putting your break point on line 20, let's put it on line 15 (at the line of code print msg
).
b 15
r
Since callback won't execute until a message is received on the chatter topic, go ahead and use rostopic pub to publish a message to the topic.
$ rostopic pub /chatter std_msgs/String "data: 'hello'"
You should see your node print out "hello" to the console. However, your script will not stop executing at the breakpoint. The reason for this is that you set the breakpoint at line 15 in one thread, and encountered line 15 in another thread. One work around for this is to use pdb.set_trace()
instead. Copy the script simple_node.py
to a new script called simple_node_modified.py
(if you don't do this, you will have trouble later). Change the callback function in simple_node_modified.py
to be:
def process_chatter(self, msg):
import pdb
pdb.set_trace()
print msg
Now, you can execute your code and it will stop as expected at the callback (you actually don't even have to execute it in PDB if you do it this way, PDB will automatically be loaded when you encounter the set_trace). To see this, run your modified simple_node.py
and publish a message to the /chatter
topic. You should be at the PDB prompt inside your callback function.
This workaround is fine, but it has one important drawback. It becomes impossible to dynamically set and unset breakpoints in your code. An alternative to this workaround is to use the winpdb module.
WinPDB
WinPDB is an advanced Python debugger. First, install it using the instructions here (basically you need to download the tar.gz, extract it, and run sudo python setup.py install -f
).
Once you have installed winpdb, try it out by running the following command:
$ winpdb ~/catkin_ws/src/comprobo15/python_debugging_practice/scripts/simple_node.py
To add a breakpoint inside your callback function and then run the script run the following commands inside winpdb.
bp simple_node.py:15
r
If you now publish a message to the /chatter topic you will gain control of your program in your callback function (as expected). This is a major advantage over PDB (the ability to set breakpoints dynamically that will be respected in any thread they are encountered). Another cool thing you can do in WinPDB is to set conditional breakpoints.
See this page for full documentation for WinPDB. Also, see this page for a nice tutorial on WinPDB.
Exercise 5
Change the breakpoint at line 15 to be a conditional breakpoint that only triggers when you publish a particular string to the /chatter
topic.
Exercise 6
Create a ROS Node that subscribes to the /scan topic. Using WinPDB, set a conditional breakpoint so that is triggered when there is an obstacle detected in front of the robot (index 0) that is less than 1m away. Verify that the breakpoint will only be triggered when this condition is met.
PDB and Bag Files
Warning: this is a creation of my own design and is somewhat experimental... Do these exercises at your own risk!
One nice usage of PDB (or WinPDB) is to debug bag files. The problem with this approach is that when you enter a breakpoint in your debugger, the bag file playback will not pause. As a workaround for this, I have created a simple utility that allows a bag file to be played back, and then pause or unpaused from your Python code.
Examine the code in python_debugging_practice/scripts/example_bag_player.py
. You will see that the callback function that processes /scan
has the following code:
def process_scan(self, scan):
""" Process scan data """
print scan.header.seq
if scan.header.seq % 10 == 0:
# for some reason we want to debug the callback every 10th scan
# pause the bag
self.bag.toggle_pause()
# enter PDB
pdb.set_trace()
# unpause the bag
self.bag.toggle_pause()
The attribute bag
contains my helper for pausing / unpausing a bag file. To see how this works, go ahead and run this node (Note: the example uses a hardcoded bag file that I have checked into the Repo, but you could easily modify it to use a different bag file of your own creation). You should see the following output and then be put at the PDB prompt:
rosrun python_debugging_practice example_bag_player.py
395
396
397
398
399
400
> /home/pruvolo/catkin_ws/src/comprobo15/python_debugging_practice/scripts/example_bag_player.py(43)process_scan()
-> self.bag.toggle_pause()
(Pdb)
If you run the c
command, you will get:
(Pdb) c
401
402
403
404
405
406
407
408
409
410
> /home/pruvolo/catkin_ws/src/comprobo15/python_debugging_practice/scripts/example_bag_player.py(43)process_scan()
-> self.bag.toggle_pause()
(Pdb)
To see how the behavior would change without pausing the bag file, modify the callback function to be:
def process_scan(self, scan):
""" Process scan data """
print scan.header.seq
if scan.header.seq % 10 == 0:
# for some reason we want to debug the callback every 10th scan
# enter PDB
pdb.set_trace()
If you run the node again, and then at the first PDB prompt run the c
command, you will notice that while you get the same output, the timing is different since the bag was never paused.
I'd love to figure out how to make this utility work a little bit better. For instance, it would be great if you didn't have to modify the source code of the Python file in order to add a breakpoint. Let me know if you have any ideas of how this could be done more cleanly.