Skip to content
Matthew Klein edited this page Jun 1, 2015 · 42 revisions

#Project 0: An Intro to ROS# In this short project we will take a quick look at the two biggest players in ROS: nodes and topics. We will write two nodes (i.e. programs) which will run simultaneously. One node will publish messages to a topic (think of a topic as a mailbox that only allows a specific type of mail and a message as one piece of that mail) and the other node will subscribe to messages published on that topic. This is the way in which nodes pass information to one another.

The cool thing is that the subscriber doesn't care who is talking to it and the publisher doesn't care who it's talking to. All they care about is getting the message out or reading the message.

An example of this on a robot is a navigation node, nav_node.cpp, and a map creation node, map_creation_node1.cpp. nav_node.cpp subscribes to a topic (the special mailbox) where only "map" messages can be placed. It needs a "map" message, because its algorithm looks at a map in order to decide where to go.

Now let's consider map_creation_node1.cpp. Suppose this node just reads a hard coded file that we put on the computer. This file contains all the information that we would want to put in a map. It then bundles that information up as a "map" message, and then publishes that "map" message to the topic that nav_node.cpp is subscribing to. Now the navigation node has something to read!

Now this is where things get interesting. Say, someone decides to write another node map_creation_node2.cpp. Instead of just reading from a file on the computer, this node gets information from cameras and LIDAR and creates a map on the fly! It formats this map as a "map" message and publishes it to the same topic as map_creation_node1.cpp. So now, we can close down map_creation_node1.cpp, run map_creation_node2.cpp, and the nav_node.cpp begins navigating with the new map without us making any changes to its code. In fact, it doesn't even know a new node is publishing those messages!

It's this ability to easily create modular, massively distributed systems that makes ROS so powerful and useful for robotics. And that's why we're going to learn how to use it!

So, onto our first project.

###Step 1: Create a Workspace###

A workspace is a folder that is organized in a standard way. It has code (.cpp and .py files) and files that help compile and build your code. Create a directory called ros_workspace/src in your home directory ~/, navigate to that folder using cd, and initialize the folder as your workspace using catkin_init_workspace.

mkdir -p ~/ros_workspace/src
cd ~/ros_workspace/src
catkin_init_workspace

You know have a CMakeLists.txt file located in ~/ros_workspace/src. This file gives the command catkin_make the instruction for compiling and building your code, even though you don't have any yet. So, let's try it out!

cd ~/ros_workspace/
catkin_make

Now we have a ~/ros_workspace/devel folder that contains a setup.bash file. Sourcing this file will overlay this workspace on top of your environment. In other words, this script allows ROS to find the packages that are contained in your workspace. Don’t forget this process will have to be done on every new terminal instance! To understand more about this see the general catkin documentation, or better yet a gentle introduction to catkin.

source ~/ros_workspace/devel/setup.bash

If you plan on always working in this workspace, add this line to ~/.bashrc. You can just open up your favorite text editor and add the line to the end of the document. This will make sure that this happens every time you open a terminal (command line). Otherwise you must type it every time.

nano ~/.bashrc

###Step 2: Create a Package### ROS bundles bits of code and other important files that have a similar purpose into things called packages. From ROS's webpage:

Software in ROS is organized in packages. A 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. The goal of these packages it to provide this useful functionality in an easy-to-consume manner so that software can be easily reused. In general, ROS packages follow a "Goldilocks" principle: enough functionality to be useful, but not too much that the package is heavyweight and difficult to use from other software.

Navigate to the src folder in your workspace and use catkin_create_pkg to create a package called minimal_nodes along with the necessary dependencies. We'll use roscpp and std_msgs in this example.

cd ~/ros_workspace/src
catkin_create_pkg minimal_nodes roscpp std_msgs

This will create a folder in ~/ros_workspace/src called minimal_nodes. Navigate to this folder and take a look at what's inside.

cd minimal_nodes
ls

You'll notice that there is another folder called src. This stands for "source" and is the folder where your actual code will reside. There is also a file called CMakeList.txt, which we will talk about later. Ignore everything else for now.

###Step 3: Write a Publisher Node###

Now it is time to actually start writing code! Navigate to this new src folder using the cd command. (cd stands for "change directory" if you haven't figured that out by now.) We now want to create a file called minimal_publisher.cpp. You can do this in a number of different ways. If you are new to this and are working from the command line, try

nano minimal_publisher.cpp

This will open up a text editor with a blank document. Copy and paste the following code into the editor. Then press "Ctr+x" to close nano, "y" to save the file, "Enter" to save the file as minimal_publisher.cpp.

#include <ros/ros.h>
#include <std_msgs/Float64.h>
int main(int argc, char **argv)
{
ros::init(argc,argv,"minimal_publisher1"); // name of this node will be "minimal_publisher1"
ros::NodeHandle n; // two lines to create a publisher object that can talk to ROS
ros::Publisher my_publisher_object = n.advertise<std_msgs::Float64>("topic1",1);
//"topic1" is the name of the topic to which we will publish
// the "1" argument says to use a buffer size of 1; could make larger, if expect network backups
std_msgs::Float64 input_float; //create a variable of type "Float64", which can be found by: roscd std_msgs
// and looking at the message type defined there.
// any message published on a ROS topic must have a pre-defined format, so subscribers know how to
// interpret the serialized data transmission
input_float.data=0.0;
ros::Rate naptime(1); //create a ros object from the ros “Rate” class; set the sleep timer for 1Hz repetition rate
while(ros::ok()) // do work here in infinite loop (desired for this example), but terminate if detect ROS has faulted
 {
 // this loop has not sleep timer, and thus it will consume excessive CPU time
 // expect one core to be 100% dedicated to this small task
 input_float.data = input_float.data+0.001; //increment by 0.001 each iteration
 my_publisher_object.publish(input_float); // publish the value--of type Float64-- to the topic "topic1"
 //next line will cause the loop to sleep for the balance of the desired period to achieve the specified loop frequency
 naptime.sleep();
 }
}

Now let's take a look at what this code actually does. The first two lines are ROS specific. ros/ros.h gives us the ability to create nodes, create topics, and publish messages to or subscribe to messages on those topics. It also gives us some time related functions.

The next is std_msgs/Float64.h, which is our first experience with a message. Messages differ slightly from plain old variables. Messages are very specific types of objects. The one here is just a simple floating point 64 bit number. However, messages can get more complicated and represent things like the robot's state or the data from a LIDAR scan. The nice thing about creating standard messages is that if we decide what a LIDAR scan message should look like, then any program that uses LIDAR data can read from any LIDAR as long as it uses the standard message type.

#include <ros/ros.h>
#include <std_msgs/Float64.h>

The first three lines of code inside the main function set up the node and a topic. ros::init initializes the node and gives it a name. We choose minimal_publisher1. However, minimal_publisher or anything else would have worked. Just make sure you don't have two nodes with the same name! This is a quick way to get strange errors when running lots of nodes at once.

Next, ros::NodeHandle create a NodeHandle object. And then we use that in the third line to create a publisher that publishes Float64s on topic1. The 1 at the end of that line indicates that number of messages that are stored in a buffer. In robotics, we often only care about the last known information. So, we will often just stick a 1 in this spot.

ros::init(argc,argv,"minimal_publisher1");
ros::NodeHandle n;
ros::Publisher my_publisher_object = n.advertise<std_msgs::Float64>("topic1",1);

We then create a variable of the type Float64 and initialize it to zero. Notice the .data part. We do this because a Float64 is actually an object and data is a parameter of that object. This may seem redundant. However, it is important as we begin to use more complicated message types.

std_msgs::Float64 input_float;
input_float.data=0.0;

Let's skip the ros::Rate command and jump into the while loop. The while loop is set to run continually until ros::ok() returns false, which indicates that ROS has halted. This will happen if you halt roscore, which we will discuss later. Inside the loop we increment our input_float.data by a little bit and then publish our message.

input_float.data = input_float.data+0.001;
my_publisher_object.publish(input_float);

And that's it! We are publishing a Float64 called input_float to topic1. Now, let's quickly revisit the code that we skipped over. First, we create an object of the Rate class called naptime. The 1 sets the sleeper timer for a 1 Hz repetition rate. Inside the loop, we call the member function `sleep()' to give the MCU one second to rest between iterations. Take this out and you will notice that all of your resources will be devoted to this one node.

ros::Rate naptime(1);
naptime.sleep();

So, that's the publisher node! We're almost at a place were we can compile and run it. But first we have to revisit the CMakeLists.txt file we saw earlier.

###Step 4: Edit `CMakeLists.txt'###

The CMakeLists.txt file contains instructions the tell catkin how to compile and build your code. You must manually include which code you want compiled and which dependencies your package needs. Remember roscpp and std_msgs. These are dependencies that our minimal_publisher.cpp node use.

So, if we cd into the folder containing our minimal_nodes package, we will see a file called CMakeLists.txt. There will be one of these in any package we make. Edit that file using your favorite text editor. (Try nano if you don't have a favorite!) You'll see a lot of stuff in there. Don't worry about it! Just scroll down to the part that says

# add_executable(minimal_nodes_node src/minimal_nodes_node.cpp)

The # sign means that that line is commented out. This line of code is actually just acting as a template. We want to write a line below it that fill is our new node name for minimal_nodes_node.

add_executable(minimal_publisher src/minimal_publisher.cpp)

Note that we didn't add a # this this line. Slightly below you will see a line that looks like this.

# target_link_libraries(minimal_nodes_node
#   ${catkin_LIBRARIES}
# )

Again, we want to replace this template with our own. (Unlike the template, I like to put everything on the same line.)

target_link_libraries(minimal_publisher ${catkin_LIBRARIES})

That's it! Now exit the editor and save your work.

###Step 5: Write a Subscriber Node###

So, we have a node that is publishing a Float64 to a topic. Let's make a node that subscribes to that topic and then prints what it hears to the terminal window. We will put this subscriber node in the same package as our publisher node. So start by navigating to the src directory in our minimal_nodes package. You can also use the ROS specific command roscd.

roscd minimal_nodes
cd src

The roscd command combs through all the packages in your environment and finds the folder you are looking for. This is part of the reason why we "source" our setup.bash files, so that commands like roscd know where to search.

In the src folder, let's create a new node called minimal_subscriber.cpp. We can do this using nano again. Copy and paste the following code into this empty file, save your file, and exit.

#include<ros/ros.h>
#include<std_msgs/Float64.h>
void myCallback(const std_msgs::Float64& message_holder)
{
 // the real work is done in this callback function
 // it wakes up every time a new message is published on "topic1"
 // Since this function is prompted by a message event, it does not consume CPU time polling for new data
 // the ROS_INFO() function is like a printf() function, except
 // it publishes its output to the default rosout topic, which prevents
 // slowing down this function for display calls, and it makes the
 // data available for viewing and logging purposes
 ROS_INFO("received value is: %f",message_holder.data);
 //really could do something interesting here with the received data…but all we do is print it
}
int main(int argc, char **argv)
{
 ros::init(argc,argv,"minimal_subscriber"); //name this node
 // when this compiled code is run, ROS will recognize it as a node called "minimal_subscriber"
 ros::NodeHandle n; // need this to establish communications with our new node
 //create a Subscriber object and have it subscribe to the topic "topic1"
 // the function "myCallback" will wake up whenever a new message is published to topic1
 // the real work is done inside the callback function
 ros::Subscriber my_subscriber_object= n.subscribe("topic1",1,myCallback);
 ros::spin(); //this is essentially a "while(1)" statement, except it
 // forces refreshing wakeups upon new data arrival
 // main program essentially hangs here, but it must stay alive to keep the callback function alive
 return 0; // should never get here, unless roscore dies
}

Let's take a look at the similarities between this subscriber node and our previous publisher node. We include some header files, initialize the subscriber node, create a NodeHandle and setup a Subscriber object. Notice that my_subscriber_object subscribes to topic1, the same topic minimal_publisher.cpp publishes to!

#include<ros/ros.h>
#include<std_msgs/Float64.h>
int main(int argc, char **argv)
{
 ros::init(argc,argv,"minimal_subscriber");
 ros::NodeHandle n;
 ros::Subscriber my_subscriber_object= n.subscribe("topic1",1,myCallback);
 ...
 return 0;
}

Now look at the declaration of the Subscriber object again. Notice that there is one more argument in the n.subscribe function than there was in the n.publish function. This is a function that is called every time my_subscriber_object receives a message on topic1. Let's look at this function in more detail.

void myCallback(const std_msgs::Float64& message_holder)
{
 ROS_INFO("received value is: %f",message_holder.data);
}

The function is called every time a message is published on topic1. This particular example doesn't do anything too interesting. ROS_INFO is a function that just prints to the console and is much more efficient that cout.

There is one last piece of interesting code that we haven't mentioned yet.

ros::spin();

This is essentially a while loop that repeats every second so that our program doesn't just end. While it is looping, it allows our callback function to be called whenever a message is published to topic1.

Just as with the publisher node, we must amend the CMakeLists.txt. Make sure you are editing the file in the minimal_nodes package root directory (i.e. ~/ros_workspace/src/minimal_nodes/CMakeLists.txt), not the one above it (i.e. ~/ros_workspace/src/CMakeLists.txt). Add the following lines:

add_executable(minimal_subscriber src/minimal_subscriber.cpp)
target_link_libraries(minimal_subscriber ${catkin_LIBRARIES})

###Step 5. Run the Nodes!###

Every time code is changed, it must be built to create executable files. Go to the root directory of the catkin workspace by typing cd ~/ros_workspace and type catkin_make. If everything goes according to plan you shouldn't see any errors. In order to register these recently built packages with ROS, type

source ~/ros_workspace/devel/setup.bash

If you added this line to your ~/.bashrc file, then this should happen every time a new bash shell (or terminal) is opened.

Now it's time to see what these nodes actually do! The first thing we have to do every time we start publishing and subscribing to messages is run roscore. Just type it at the terminal and press enter. You can then open up a new terminal or press Ctrl+Z followed by bg to make it run in the background.

roscore

Next, let's run our two nodes using the rosrun <package> <node> command.

rosrun minimal_nodes minimal_publisher

Open another terminal or send minimal_publisher to the background with Ctrl+Z followed by bg and run minimal_subscriber.

rosrun minimal_nodes minimal_subscriber

The output should look something like the following:

[ INFO] [1433193951.116144032]: received value is: 0.022000

If you see this, then your publisher and subscriber nodes worked! What exactly is happening? The publisher is adds 0.001 to a value, places that value into a Float64 message, and publishes that message to the topic /topic1. The subscriber subscribes to this topic and every time a message is published to it the callback function triggers and prints the ROS_INFO message to console.

(If you sent processes to the background with Ctl+Z and bg, bring them back to the forground using fg and close them using Ctl+C. Type jobs to make sure you got everything.)

###Step 6. Create a Launch File###

All that roscore and background/forground stuff was kind of annoying. If you plan on launching several nodes at the same time often, it makes sense to create a launch file.

In the root directory of the minimal_nodes package create a new launch directory and cd into it.

cd ~/ros_workspace/src/minimal_nodes
mkdir launch
cd launch

Next create a file called minimal_launcher.launch using a text editor and copy and paste the following:

<launch> 
 <node name= "minimal_publisher" pkg= "minimal_nodes" type= "minimal_publisher" /> 
 <node name= "minimal_subscriber" pkg= "minimal_nodes" type= "minimal_subscriber" /> 
</launch>

Save and close the file. Then type

roslaunch minimal_nodes minimal_launcher.launch

Everything that happened in the previous section should happen here, but with only a single command! Launch files are very powerful and can do much more than this. But, this shows how useful even a basic launch file can be.

###7. What's next?###

If you want more (and I hope you do!), here is a very thorough guide to ROS.

Clone this wiki locally