In ROS, a node is a process that performs a computation. A robot system will usually comprise of many nodes. For example, one node may control a laser range-finder, another node send commands to the robot's wheel motors, another node performs localization, another node performs path planning, and another node provides a graphical view of the system.
The use of nodes in ROS provides several benefits to the overall system. Code complexity is reduced in comparison to monolithic systems as distinct functionality is encoded in each node. Implementation details are also well hidden as the nodes expose a minimal API to the rest of the graph, and alternate node implementations, even in other programming languages, can easily be substituted facilitating reuse. There is also additional fault tolerance as crashes are isolated to individual nodes.
Topics are named buses over which nodes exchange messages. Topics have anonymous publish/subscribe semantics, which decouple the production of information from its consumption. In general, at the logical level, nodes are not aware of who they are communicating with. Instead, nodes that are interested in the data coming through the relevant subscribed topic; nodes that generate data publish to the relevant topic. There can be multiple publishers and subscribers to a topic.
Nodes and topics constitute the bases for ROS Publish/Subscribe architecture, the focus of this lab. However, note that ROS also provides support for services and actions
Source: ROS Wiki: Nodes and ROS Wiki: Topics
At the end of this lab, you should understand:
Starting this lab, we are going to make improvements to our rocketship from Lab 1. First, let's get the Lab 2 workspace. Remember a ROS workspace is a folder where you modify, build, and install ROS code. This new workspace contains Lab 1's code with the modifications you should have made to Lab 1. To clone the new workspace, you can run the following in a terminal:
# Change to home directory
$ cd ~
# Clone the code
$ git clone https://github.com/hildebrandt-carl/RSECS4501-Spring2020-Lab2.git
# Get the labs workspace
$ mv RSECS4501-Spring2020-Lab2/lab2_ws ~/lab2_ws
# Do some cleanup
$ rm -rf RSECS4501-Spring2020-Lab2
In Lab 1, when we wanted to abort the rocket launch, we had to type a command in the terminal to publish a message. This was particularly slow and cumbersome. Being able to abort the rocket is vital and therefore should be easier to do. Let's update our rocket system to abort if a particular key on our keyboard is pressed, as opposed to typing in an separate terminal command.
To do this, we will build two new nodes. The first node will check if a key on the keyboard has been pressed, and then publish a message with a code representing the pressed key. The second node will check the key's code, and if the key matches some criteria, abort the rocket.
The first node needs to check what key on the keyboard was pressed. This is a common generic task, and thus, there are probably already many existing keyboard nodes for us to reuse. Remember, ROS makes reusing code easy, so if you need to do a common task, someone has already probably done it, embedded into a node, and made it available for others to reuse. Even a general google search for "ROS keyboard node" returns useful results. One of the first pages we see is: https://wiki.ros.org/keyboard. You can see a screenshot of the webpage below:
Skimming through the documentation, we pull out three important pieces of information:
Let's download this node and see if we can use it. Search the internet for "ROS keyboard node GitHub". Depending on your search engine, one of the first pages you will see is: https://github.com/lrse/ros-keyboard. You will notice that there isn't much documentation on the Github page. That means we will need to spend some time figuring out how to use this code, but it looks simple enough to explore it further. Clone the ROS package with git and compile our workspace. To do that run the following:
# Go into the source folder of our workspace
$ cd ~/lab2_ws/src
# Clone the keyboard package
$ git clone https://github.com/lrse/ros-keyboard.git
# Go back to the main directory
$ cd ~/lab2_ws
# Build the catkin workspace
$ catkin build
The compilation will probably fail. Scroll up through your build logs, and you will see the following error:
__________________________________________________________
Errors << keyboard:cmake /home/robotclass/lab2_ws/logs/keyboard/build.cmake.000.log
CMake Error at /usr/share/cmake-3.5/Modules/FindPackageHandleStandardArgs.cmake:148 (message):
Could NOT find SDL (missing: SDL_LIBRARY SDL_INCLUDE_DIR)
Call Stack (most recent call first):
/usr/share/cmake-3.5/Modules/FindPackageHandleStandardArgs.cmake:388 (_FPHSA_FAILURE_MESSAGE)
/usr/share/cmake-3.5/Modules/FindSDL.cmake:199 (FIND_PACKAGE_HANDLE_STANDARD_ARGS)
CMakeLists.txt:5 (find_package)
It is important to learn how to read error messages as you may find yourself in similar situations in the future. At first, this error message might seem overwhelming. However, after you read through it, you will notice that all we are missing is a specific library: SDL_Library
. We can search on how to install these libraries online. For instance, we can search: "missing: SDL_LIBRARY SDL_INCLUDE_DIR". Using this search led me to this page: https://github.com/ros-planning/navigation/issues/579. Although this page is not related to the node we are building, the error is the same, and the solution provided (if you want to learn more about linux dependencies management, check the apt-get doc).
$ sudo apt-get install libsdl-dev -y
Now that we have installed the required libraries let's recompile our workspace.
$ catkin build
This time everything should compile. Next, let's figure out how this node works. Figuring out how a node works can generally be done statically by inspecting the code for particular calls to publishing and subscribing to topics, or also dynamically by following these steps: 1) running the node, 2) listing the available topics, 3) publishing, or echoing topics specific to that node. In our case, we know from the documentation that the node should publish messages on either /keydown
or /keyup
, and so we know we need to echo those topics. Open three terminals and run the following commands:
Terminal 1
$ roscore
Terminal 2
$ source ~/lab2_ws/devel/setup.bash
$ rosrun keyboard keyboard
Terminal 3
$ source ~/lab2_ws/devel/setup.bash
$ rostopic echo /keyboard/keydown
Click on the little GUI that popped up and press a key on the keyboard (for instance, press the key "a"). In terminal 3, you should see the following:
>>> ...
>>> code: 97
>>> modifiers: 0
>>> ---
What did we just do? Well, we clicked a key on our keyboard, the ROS node keyboard registered that keypress, and it published a ROS message on the topic /keyboard/keydown
. Try pressing other keys and looking at what the published ROS messages are. Below is our computation graph. We can see that the keyboard node is publishing both /keyboard/keydown
and /keyboard/keyup
topics, and we are echoing one of those topics.
Note on convention: you will notice that the keyboard node's topic starts with the keyword /keyboard/
. This is because the keyboard node appends its name to the beginning of the topic. This is something you, as a developer, can decide to do in ROS. You can do more reading up on this convention on the ROS wiki or this ROS question. For the rest of this tutorial, when we refer to /keydown
we are referring to the entire topic name /keyboard/keydown
.
Now that we have figured out how the keyboard node works, close it so we can develop a keyboard manager. Close all terminals we just opened. The keyboard manager's job will be to take the information provided by the keyboard node, interpret it, and then publish appropriate control messages to the rocket (for instance, /launch_abort
). In our case, we want to check if the "a" key was pressed and then send an abort message. Start by creating the node:
# Create the keyboard-to-abort manager node
$ touch ~/lab2_ws/src/rocketship/src/keyboard_manager.py
# Give the keyboard-to-abort manager node execution privileges
$ chmod u+x ~/lab2_ws/src/rocketship/src/keyboard_manager.py
Next, we are going to open the node we just created so that we can edit the content. You can open the file in any text editor of your choice. The provided virtual machine has Visual Studio Code installed. To open the file in Visual Studio Code, you can either type this in the terminal or click the blue icon in the menu bar.
$ code ~/lab2_ws
Let's think through how we want to design this node. We know that we want to publish /launch_abort
messages to the rocket. We also know that we want to subscribe to /keydown
messages that are published by the keyboard node. The goal of the node will be to process the /keydown
messages, and if the letter "a" was pressed, send an abort message. We can see this design overview below:
You might be tempted to ask the question: "Why did we not just edit the source code of the keyboard node to publish abort messages directly?". The answer is, we could have. However, this would defeat some of the decoupling benefits provided by ROS.. For instance, if we make a mistake in our keyboard_manager
node and it crashes, 1) no other nodes will crash, creating some fault tolerance, 2) we as developers know that the fault has to be inside that specific node, and 3) when we look into that node to fix it, the node will be relatively simple as its implementation is on a single simple task. Also, if we find a better keyboard node (i.e., one that support a larger character set) we could easily replace it.
Now let's figure out how we will implement this node.
The first thing we note is that a key could be pressed at any time. Thus we will need to repeatedly check if the key is pressed. This continuous checking is implemented by having a main loop inside the node.
Next, we note that ROS's communication works asynchronously. That means that ROS messages can be sent at any time. ROS handles asynchronous communication using callback functions. These are functions that we implement but never call directly. Callback functions are called in our node anytime a message is received. We can then use variables to pass the information received in our callback to our main loop, which will determine what key was pressed and respond accordingly.
Graphically that would look like this:
Declare the Node:
Let's figure out the implementation details for each of these sections. The first thing we do is declare the node. To declare a node, we call the init_node
function. This function registers the node with the roscore
, thus allowing it to communicate with all other nodes.
rospy.init_node('keyboard_manager_node')
Class Initialization:
During class initialization, we create the publishers, subscribers, and ROS messages we are going to be using. To create a publisher, for instance, to send messages to our abort system, we use the command:
# rospy.Publisher(<Topic Name>, <Message Type>, <Queue Size>)
rospy.Publisher('/launch_abort', Bool, queue_size=1)
A publisher needs a topic name, what message type it will be sending, and queue size. The queue size is the size of the outgoing message queue used for asynchronous publishing. To create a subscriber, for instance, to subscribe to keyboard messages, we do the following:
rospy.Subscriber(<Topic Name>, <Message Type>, <Callback Function>, <Queue Size>)
rospy.Subscriber('/keyboard/keydown', Key, self.get_key, queue_size=1)
Subscribers have function declarations similar to publishers, but there is one key difference. Subscribers also have the name of the callback function. That is, every time this subscriber receives a message, it will automatically invoke the callback function and pass the received message to that function.
The last part of class initialization is to create the message we will be publishing. To create a message, we call the constructor of the message we want to create. In our case, we are using the standard ROS messages, std_msgs. More specifically, we are using the boolean message type, Bool (provided by ROS's std_msgs). This message type has a single attribute "data" which is of the python type bool. Message types will be covered more in the next lab.
abort = Bool()
Class Variables:
The class variable we will be using will take the key code from the callback and save it as a class variable. Doing this will give the entire class access to the variable. Creating a class variable can be done in Python using the self
keyword. For instance:
from std_msgs.msg import Bool
self.key_code = -1
Callbacks:
Remember that ROS uses asynchronous communication. That means, at any time, our node could receive a message. To handle asynchronous communication, you need to create callbacks. You never call this function yourself, but whenever a message arrives, the function will be invoked. You want to keep callback functions as short as possible to not interrupt the flow of your main program. For instance, in our case, all we need to do is to save the key's code from each keypress. To do that, we can create our function to take the ROS message and save it to our class variable.
def get_key(self, msg):
self.key_code = msg.code
Main Loop:
Finally, in our main loop, we want to check if a keypress repeatedly. So for instance, if we wanted to check if the letter "b" was pressed we could use:
# Check if "b" key has been pressed
if self.key_code == 98:
# "b" key was pressed
print("b key was pressed!")
When the correct letter is pressed, we want to set our abort message to true and publish it. Remember, our Bool message had a single attribute data, which was of the Python type bool. We thus use the following:
abort.data = True
abort_pub.publish(self.abort)
At the end of each loop, we want to check for allow time for callbacks. In Python's ROS API, callbacks are checked during the time the main loop sleeps. To do this, we use a sleep function. The sleep function provides one other vital role. The sleep function allows us to control at what rate our main loop operates.
rate.sleep()
Final Code:
Putting this all together, we get the final code. Note: there is a single piece you need to fill in marked with #TODO. Use what we learned from the keyboard section to complete this.
#!/usr/bin/env python
import rospy
from keyboard.msg import Key
from std_msgs.msg import Bool
# Create a class which we will use to take keyboard commands and convert them to an abort message
class KeyboardManager():
# On node initialization
def __init__(self):
# Create the publisher and subscriber
self.abort_pub = rospy.Publisher('/launch_abort', Bool, queue_size=1)
self.keyboard_sub = rospy.Subscriber('/keyboard/keydown', Key, self.get_key, queue_size=1)
# Create the abort message we are going to be sending
self.abort = Bool()
# Create a variable we will use to hold the keyboard code
self.key_code = -1
# Call the mainloop of our class
self.mainloop()
# Callback for the keyboard sub
def get_key(self, msg):
self.key_code = msg.code
def mainloop(self):
# Set the rate of this loop
rate = rospy.Rate(5)
# While ROS is still running
while not rospy.is_shutdown():
# Publish the abort message
self.abort_pub.publish(self.abort)
# Check if any key has been pressed
if self.key_code == #TODO:
# "a" key was pressed
print("a key was pressed!")
self.abort.data = True
# Reset the code
if self.key_code != -1:
self.key_code = -1
# Sleep for the remainder of the loop
rate.sleep()
if __name__ == '__main__':
rospy.init_node('keyboard_manager_node')
try:
ktp = KeyboardManager()
except rospy.ROSInterruptException:
pass
Now that we have created the node, we need to test if it works. Let's test if when the letter "a" is press on the keyboard, an abort is sent. Launch for terminals:
Terminal 1
$ roscore
Terminal 2
$ source ~/lab2_ws/devel/setup.bash
$ rosrun keyboard keyboard
Terminal 3
$ source ~/lab2_ws/devel/setup.bash
$ rosrun rocketship keyboard_manager.py
Terminal 4
$ source ~/lab2_ws/devel/setup.bash
$ rostopic echo /launch_abort
Click on the GUI that was launched with the keyboard node. Press multiple keys on your keyboard, but avoid hitting "a". You should notice that the messages published on the topic /launch_abort
topic are:
>>> data: False
>>> ---
>>> data: False
>>> ...
However, as soon as you hit the "a" key you should get:
>>> data: True
>>> ---
>>> data: True
>>> ...
Awesome, we have now created a system that monitors your keyboard and sends an abort command to our rocket after we press the "a" key!
We just downloaded a node keyboard with the name keyboard in the keyboard package. We also created a node keyboard_manager with the name KeyboardManager in the rocketship package. Using what you learned from Lab 1, edit the launch file rocket.launch
so that when we launch the rocket, both the keyboard
and the keyboard_manager
are also run.
Using the rocketship launch file, launch the rocket. To confirm that you have launched the rocket correctly, use rqt_graph
to display the computation graph.
1) How does the computation graph differ from lab 1? Why?
2) Launch the rocket and use a key (letter "a") to abort the rocket. Does your rocket abort?
3) What does each node publish or subscribe to?
For the remainder of this class, we will be using a Crazyflie quadrotor to learn key robot development concepts in a concrete platform . The Crazyflie 2.0 is a versatile open-source flying development platform that only weighs 27g and fits in the palm of your hand. Before we start flying these quadrotors in the real-world, let's make sure we can fly them in simulation.
We will be using a simulator that simulates the Crazyflie. The simulator was designed by the UAV activities at the Centre for Autonomous Systems Lab and was used in their robotics course. We have made some modifications to the simulator to allow it to run on our systems. Before we launch the simulator, we need to compile the workspace that contains the Crazyflie simulator.
$ cd ~/crazysim_ws
$ catkin build
If everything builds successfully, you can launch the simulator by running the following command. You will notice we use a launch file to launch the simulator. By now, you should start to understand how important launch files are. Take a minute and look at the launch file and see if you can tell what nodes are being launched. The launch file is located in ~/crazysim_ws/src/course_packages/dd2419_launch/launch
.
$ source ~/crazysim_ws/devel/setup.bash
$ roslaunch dd2419_simulation simulation.launch
It might take a little while to launch the first time. Once it is launched, you will see something like the image below. You are looking at a Gazebo simulation. Gazebo is a simulation engine that is often used with ROS. Gazebo offers the ability to accurately and efficiently simulate populations of robots in complex indoor and outdoor environments. Gazebo uses both a physics engine and graphics rending software to both accurately simulate the physics of the robot and display how they would behave in the real world.
You can see the drone in the center of the screen where the red, green, and blue lines meet. These lines are the x,y, and z-axis in our simulation
To navigate the camera you can do the following:
Move the camera behind the quadrotor and zoom in. Try and replicate the image below, rememeber to take a screenshot.
Now let's try and fly the drone. We will be using a very similar process to that of Lab 1. In Lab 1 the rocket had a node that subscribed to the cmd_vel
topic, and to fly the rocket, we published commands to cmd_vel
. We are going to do exactly that here. Let's see what topics are currently available. Open a terminal and list the available commands as per the list of topics:
$ source devel/setup.bash
$ rostopic list
>>> ...
>>> /cf1/cmd_position
>>> ...
Using rqt_publisher
, publish messages to /cf1/cmd_position
at 20Hz. Remember to use the source command to setup the environment variables for the workspace! Fly the drone using rqt_publisher
, navigate it through one of the openings in the orange wall. Take a screenshot similar to this:
The final part of this lab is to get the drone flying using the arrow keys on our keyboard. The first step will be to download the keyboard
package into this workspace. This will be your first time working independently inside your workspace. Remember that a workspace is a folder that contains all the files for your project. Inside the source folder of your workspace, you will find packages. Each package might contain ROS nodes, a ROS-independent library, a dataset, configuration files, a third-party piece of software, or anything else that logically constitutes a useful module. Inside each of the packages source folders, you will find the source code of each of the nodes. Below is an example of a simple workspace, and a simplified version of our final crazysim workspace.
Once you have downloaded the keyboard package, make sure your workspace compiles by running:
$ catkin build
We learned earlier that the Crazyflie can be controlled using the /cf1/cmd_position
topic. Next lets add the keyboard
package to this workspace. Clone the keyboard
node into this workspace.
Once this is done our next task will be to revise the keyboard manager node so that it takes the keyboard arrow keys and flies the drone. We start by creating a new simple_control
package:
$ cd ~/crazysim_ws/src
$ catkin_create_pkg simple_control std_msgs rospy
Next, create the keyboard_manager
node using a process similar to that we used earlier with the rocketship. Place the keyboard_manager
inside the simple_control
package. Once you have created the keyboard_manager
code, open it for editing. We will provide you with some skeleton code that you need to complete. The sections you need to complete are marked using the comment #TODO.
#!/usr/bin/env python
import rospy
import time
from keyboard.msg import Key
from crazyflie_gazebo.msg import Position
# Create a class which we will use to take keyboard commands and convert them to a position
class KeyboardManager():
# On node initialization
def __init__(self):
# Create the publisher and subscriber
self.position_pub = rospy.Publisher('#TODO', Position, queue_size=1)
self.keyboard_sub = rospy.Subscriber('#TODO', Key, self.get_key, queue_size = 1)
# Create the position message we are going to be sending
self.pos = Position()
# Start the drone a little bit off the ground
self.pos.z = 0.5
# Create a variable we will use to hold the key code
self.key_code = -1
# Give the simulation enough time to start
time.sleep(10)
# Call the mainloop of our class
self.mainloop()
# Callback for the keyboard sub
def get_key(self, msg):
self.key_code = msg.code
def mainloop(self):
# Set the rate of this loop
rate = rospy.Rate(20)
# While ROS is still running
while not rospy.is_shutdown():
# Publish the position
self.position_pub.publish(self.pos)
# Check if any key has been pressed
# TODO
# Reset the code
if self.key_code != -1:
self.key_code = -1
# Sleep for the remainder of the loop
rate.sleep()
if __name__ == '__main__':
rospy.init_node('keyboard_manager')
try:
ktp = KeyboardManager()
except rospy.ROSInterruptException:
pass
Hint: It might be easier to launch the simulation, keyboard, and keyboard_manager in separate terminals for testing to quickly identify where the any faults are coming from by looking at which terminal crashed.
Finally once you are done testing the basic functionality, add these lines to the end of the simulations launch (crazysim_ws/src/course_packages/ dd2419_simulation/launch/simulation.launch
) file so that you can run the code using a single command:
<?xml version="1.0"?>
<launch>
<arg name="mav_name" default="crazyflie"/>
...
<node name="keyboard_manager_node" pkg="simple_control" type="keyboard_manager.py" output="screen" />
<node name="keyboard" pkg="keyboard" type="keyboard" output="screen" />
</launch>
Demonstrate that you can launch the Crazyflie simulator, keyboard node, and keyboard manager using a single launch file. Subsequently, show that you can fly the Crazyflie around using the arrow keys on your keyboard. Below is a sped-up example of how your demonstration should look. (Note: The virtual keyboard is shown for illustration, for your demonstration use your computer's keyboard.)
Congratulations, you are done with lab 2!
At the end of this lab, you should have the following:
rqt_graph
to display the communication graph for the updated rocketship.rqt_publisher
to fly the Crazyflie into any of the square openings in the orange walls. Take a screenshot of your Crazyflie hovering in one of the openings.