Skip to content
pryre edited this page Feb 22, 2020 · 26 revisions

What is ROS?

The Robotic Operating System (ROS) is a management solution for real-time robotic software. ROS provides simple and robust interfaces for communication between software, while allowing for the complete logging of the system. Most popular software packages, such as OpenCV, Mavlink, Python, and MatLab, have a ROS compatible component to allow for easy integration. Additionally, there is a huge amount of user packages available on the community network.

Why use ROS?

The primary usefulness of ROS is in the libraries available as part of the ROS environment, and how simple they make it to develop modular and interchangeable software. The idea behind developing software in this way is that it allows us to develop both tools that are useful for multiple use cases, as well as tools that are very good at solving a specific problem with as little complexity in the specific node, which aids in development, debugging, and understanding for others. For the most systems, there is no noticeable delay within the system, and if best conventions are followed, a highly dynamic system can be developed without many losses in performance.

The use of different Nodes to handle different problems also allows ease of splitting and integrating intermediate software or data visualizations without any additional programming. It also allows use to collect and analyze data after the experiment and replay the results in "real-time", which allows accurate testing of different methods on real data with no additional effort.

ros_concepts

Another major advantage of ROS is that it provides access as a Distributed Network. Each and any node can be ran on different hardware, but will still communicate as if it were all on the same computer, if the network is properly configured. For this reason, a navigation controller and user interface can be ran on a GCS, while the flight control software is ran on-board with the image processing, while the images are post-processed on a second GCS and made available over a web interface. This drastically simplifies the work needed to write interfaces such as sockets and data streams to pass information between the separate systems.

Useful Resources

As of the time of writing, it is probably best to use ROS Kinetic (and Ubuntu 16.xx as a result). This is as Kinetic is the current Long Term Release, and should offer the most support for any issues.

For more information, please check: http://wiki.ros.org/

In the installation guide of ROS, it recommends that you install the full desktop stack for the best functionality. It may be more convenient however to install a baseline version, for example if you only need to run ROS on an on-board computer.

For more information on the different meta-packages ROS offers, please check: http://www.ros.org/reps/rep-0142.html#robot-metapackage

ROS Commands & Tools

This section will briefly outline some of the more standard commands and tools available through ROS, and how they can be used.

Unless specifically stated, most of the following tools can be run from any directory, as they will automatically locate the necessary folders/packages within your environment.

roscore

The roscore is the launches the ROS master interface, which organizes all running Nodes, and will handle service requests, among other things. It must be run to start the ROS environment (with the only exception being that the roslaunch tool will start a roscore in the background if one is not running already).

For more information, check the ROS Wiki.

Usage:

$ roscore

rosrun

The rosrun command will automatically search for, and allow you to launch programs and Nodes from, specific packages. As long as you have your ROS installation sourced (most likely done automatically with your .bashrc file), as well as any catkin workspaces, etc., rosrun will be able to find packages, and launch programs, scripts, etc., from within them.

Many packages will offer helper tools and scripts that can be run with rosrun. It is also a good way to quickly test software during development.

For more information, check the ROS Wiki.

Usage:

$ rosrun package_name program_name

roscd / roscp / rosed

These tools can be used to perform specific functions on packages and files within your environment without the need to directly locate them in your filesystem.

Usage - roscd:

Used to change directory into a package:

$ roscd package_name

Usage - rosed:

Directly edit a file from a package (you need to have the $EDITOR bash variable set to your text editor of choice first):

$ rosed package_name file_name.launch

Usage - roscp:

Copy a file from a package to somewhere else (acts very similar to the standard cp command in Linux): To copy a file to your current directory:

$ roscp package_name file_name.launch ./

To copy a file somewhere else, and rename it if desired (multiple examples):

$ roscp package_name file_name.launch /home/user/file_name.launch
$ roscp package_name file_name.launch ~/catkin_ws/launch/new_name.launch

For more information, check the ROS Wiki.

roslaunch

The roslaunch package allows you to run a launch script to assist with automation, setting Node parameters, as well as many other features. It is extremely helpful when configuring a project that requires many different Nodes to be run, and will manage all of them through a single terminal.

Most packages will provide example launch files which can be used to test or simply as a base to create your own launch files. It is highly recommended that you copy these files to a local directory before you modify them, as any updates to your system (for that package) may overwrite changes to the files.

For more information, check the ROS Wiki.

Usage:

There are two different ways to run launch files; from a package, or with a file path.

From a package:

$ roslaunch package_name name.launch

With a file path (multiple examples):

$ roslaunch /home/user/file.launch
$ roslaunch ~/catkin_ws/launch/file.launch
$ roslaunch /usr/share/ros/launch/file.launch

rostopic / rosservice

These packages give access to a collection of tools to perform checks on topics, transferred data, generating fake data, run services from the command line (great for say, remotely arming a UAV, or commanding a mission to begin), as well as a lot of other diagnostic information.

For more information on rostopic, check the ROS Wiki.

For more information on rosservice, check the ROS Wiki.

rqt / rviz

These packages can allow for assembling a nice GUI, or easy visualization of data. both programs allow you to add in additional plugins to enhance the experience.

For more information on rqt, check the ROS Wiki.

For more information on rviz, check the ROS Wiki.

The Catkin Workspace

The catkin workspace is an area for you to download, develop, compile, and test ROS nodes. Instructions on the primary setup can be found here, you should follow these steps before continuing.

The catkin setup neglects to mention some steps to automation, specifically, the fact that you must tell ROS where your packages are each time you open a new terminal. You can which packages ROS can see by opening a new terminal an running the command:

echo $ROS_PACKAGE_PATH

In the output shown, you should see the directory that is your catkin workspace. If you do not, it is because the process of adding your catkin workspace is not automated. To automate the process, run the following command:

echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc

Open a new terminal, then run the echo command from before. Now your catkin workspace should be listed. If you have set up the catkin workspace to be added automatically using this method, all the included packages should be found automatically each time you open a new terminal.

Writing ROS Nodes

As previously described, ROS Nodes are simply individual programs that use ROS to communicate behind the scenes. This section will be a brief overview and breakdown of a basic Node written in both C++ and Python. In the C++ case, there are multiple ways in which this type of node could be constructed, but for the purposes of similarity, we will look at a class-based subscriber/publisher.

For more information on the programming in ROS, and the other sorts of things that are possible, please refer to the ROS Wiki.

The source files for the following examples can be found in the kinetic_sample_packages repository. Before going to check these out, make sure you are familiar with the ROS package structure.

ROS Node Example

Below, we have the source code for the most basic main of a Node (comments have been removed for brevity). We will also include the class in the same file (where as in the source examples, they are included from a separate class file).

Both examples have been kept as similar as possible, and as such, only the python code will be explained in depth. It assumes that you are familiar with proper python layout.

Python Node

#!/usr/bin/env python
import rospy

...

if __name__ == '__main__':
	rospy.init_node('spinner_py', anonymous=True)

	sp = Spinner()

	try:
		rospy.spin()
	except rospy.ROSInterruptException:
		sp.shutdown()

C++ Node

#include <ros/ros.h>

...

int main(int argc, char** argv) {
	ros::init(argc, argv, "spinner_cpp");

	Spinner sp;

	ros::spin();

	return 0;
}

Node Explanation

rospy.init_node('spinner_py', anonymous=True)

This creates initializes the actual ROS node, and allows for communications with the roscore. This should be called before your node attempts to set up any publishers, subscribers (etc.). In C++, you must call this before calling any other ROS interfaces.

sp = Spinner()

This creates an object of the class Spinner (this is discussed below). In a Node of this complexity, this object will run all the functionality of our node.

try:
	rospy.spin()
except rospy.ROSInterruptException:
	sp.shutdown()

The use of the spin() function tells the underlying Node to listen for any incoming messages and act on them accordingly. This effectively runs an infinite loop (while(True)), but allows ROS to handle anything else it needs to do in the background. Without this, the callbacks in the Spinner class that are triggered when new messages arrive will never be called.

The rospy.spin() function will lock the thread until something causes the Node to quit. In our case, we wait for the rospy.ROSInterruptException, which is usually a "CTRL+C" key-press from the user, in which case, we should shutdown the the class, and allow the Node to exit.

In the C++ example, we only need to call ros::spin();, as the class destructors will handle the shutdown sequence automatically.

Spinner Class

In the previous code, the Spinner class was used to handle all of the actual functionality of our Node. For the purposes of this example, our Node will perform 2 different functions to demonstrate the capabilities of ROS. This goes back to the previous discussion that a Node can have as many publishers and subscribers as it needs to.

First off, we will create a timer to publish a message (a std_msgs/Empty) at a set interval. The reason for doing this over other methods that we can (reasonably) reliably have a function that executes every so often, and it also runs on the callback queue (i.e. as long as we do rospy.spin(), then the timer will run without interrupting our callbacks).

Secondly, we will create a subscriber and publisher to listen for a specific type of message (a geometry_msgs/TransformStamped), and the re-publish the data as a different message type (a 'geometry_msgs/PoseStamped').

Python Spinner Class

from std_msgs.msg import Empty
from geometry_msgs.msg import TransformStamped
from geometry_msgs.msg import PoseStamped

...

class Spinner():
	def __init__(self):
		self.timer = rospy.Timer(rospy.Duration(1.0), self.callback_ping)
		self.pub_ping = rospy.Publisher('/ping', Empty, queue_size=10)

		self.sub_transform = rospy.Subscriber("/transform", TransformStamped, self.callback_transform)
		self.pub_pose = rospy.Publisher('/pose', PoseStamped, queue_size=10)

	def shutdown(self):
		self.sub_ping.unregister()
		self.timer.shutdown()

	def callback_ping(self, timer_ev):
		msg_out = Empty()
		self.pub_ping.publish(msg_out)

	def callback_transform(self, msg_in):
		msg_out = PoseStamped()
		
		msg_out.header = msg_in.header
		msg_out.pose.position.x = msg_in.transform.translation.x
		msg_out.pose.position.y = msg_in.transform.translation.y
		msg_out.pose.position.z = msg_in.transform.translation.z
		msg_out.pose.orientation.x = msg_in.transform.rotation.x
		msg_out.pose.orientation.y = msg_in.transform.rotation.y
		msg_out.pose.orientation.z = msg_in.transform.rotation.z
		msg_out.pose.orientation.w = msg_in.transform.rotation.w
		
		self.pub_pose.publish(msg_out)

C++ Spinner Class

#include <std_msgs/Empty.h>
#include <geometry_msgs/TransformStamped.h>
#include <geometry_msgs/PoseStamped.h>

...

class Spinner {
	private:
		ros::NodeHandle nh_;
		ros::Timer timer_;
		ros::Publisher pub_ping_;
		ros::Subscriber sub_transform_;
		ros::Publisher pub_pose_;

	public:
		Spinner() :
			nh_("~") {

			timer_ = nh_.createTimer(ros::Duration(1.0), &Spinner::callback_ping, this );
			pub_ping_ = nh_.advertise<std_msgs::Empty>("/ping", 10);
			
			sub_transform_ = nh_.subscribe<geometry_msgs::TransformStamped>( "/transform", 10, &Spinner::callback_transform, this );
			pub_pose_ = nh_.advertise<geometry_msgs::PoseStamped>("/pose", 10);
		}

		~Spinner() {
		}

		void callback_ping(const ros::TimerEvent& e) {
			std_msgs::Empty msg_out;
			pub_ping_.publish(msg_out);
		}

		void callback_transform(const geometry_msgs::TransformStamped::ConstPtr& msg_in) {
			geometry_msgs::PoseStamped msg_out;

			msg_out.header = msg_in->header;
			msg_out.pose.position.x = msg_in->transform.translation.x;
			msg_out.pose.position.y = msg_in->transform.translation.y;
			msg_out.pose.position.z = msg_in->transform.translation.z;
			msg_out.pose.orientation.x = msg_in->transform.rotation.x;
			msg_out.pose.orientation.y = msg_in->transform.rotation.y;
			msg_out.pose.orientation.z = msg_in->transform.rotation.z;
			msg_out.pose.orientation.w = msg_in->transform.rotation.w;

			pub_pose_.publish(msg_out);
		}
};

Spinner Class Explanation

def __init__(self):
	self.timer = rospy.Timer(rospy.Duration(1.0), self.callback_ping)
	self.pub_ping = rospy.Publisher('/ping', Empty, queue_size=10)

	self.sub_transform = rospy.Subscriber("/transform", TransformStamped, self.callback_transform)
	self.pub_pose = rospy.Publisher('/pose', PoseStamped, queue_size=10)

As previously stated, this Node will be doing two tasks simultaneously; sending out a ping at a set rate, and transforming data from one type to another. The two groupings of functions above represent this respectively

The first two objects created are for the timer. Firstly, we set up a the timer to run the function callback_ping with a update rate of rospy.Duration(1.0) (once per second). Secondly, we set up a publisher so we can send a Empty message on the topic "/ping" when we want to.

The second two objects created are for the data conversion. Firstly we set up a subscriber to listen for TransformedStamped messages on the topic "/transform", and run the self.callback_transform function whenever one is received. Secondly, we set up a publisher so we can send a PoseStamped message on the topic "/pose" when we want to.

Note that both of the functionalities that have been set up in this class rely on the rospy.spin() function that is being run in the main loop. Without this, this class will do nothing, as it is never told to activate the timer, or to check for new data from the subscriber.

def shutdown(self):
	self.sub_ping.unregister()
	self.timer.shutdown()

In Python, we need to be self-conscious of the subscribers and timers we create, as Python will note clean up for us afterwards. To ensure nice functionality from our class, we need to remember to unregister and shutdown the subscribers and timers (the Spinner shutdown function is called from the main when we want to quit the Node).

def callback_ping(self, timer_ev):
	msg_out = Empty()
	self.pub_ping.publish(msg_out)

When the timer decides that it is time to run the function, we simply create the output message, and then publish it directly. We could make use of the timer_ev variable, which contains information about when the timer was called, how long since the last timer, etc., but for this purpose, it is ignored.

def callback_transform(self, msg_in):
	msg_out = PoseStamped()
	
	msg_out.header = msg_in.header
	msg_out.pose.position.x = msg_in.transform.translation.x
	msg_out.pose.position.y = msg_in.transform.translation.y
	msg_out.pose.position.z = msg_in.transform.translation.z
	msg_out.pose.orientation.x = msg_in.transform.rotation.x
	msg_out.pose.orientation.y = msg_in.transform.rotation.y
	msg_out.pose.orientation.z = msg_in.transform.rotation.z
	msg_out.pose.orientation.w = msg_in.transform.rotation.w
		
	self.pub_pose.publish(msg_out)

When the subscriber gets called, we are provided with the msg_in variable. This contains all the data that was received across the topic. This variable is a simple structure of the type TransformStamped (as an example, check here for the message definition).

As previously described, the purpose of this callback is convert the message from one type to another. When new data is received, we create an output message, copy across the relevant data from the input to the output, then publish the new message.

Distributed ROS

A key idea behind ROS is that it always runs assuming it was on a distributed network. When this is not the case, all information in ROS points towards "localhost". What this means is that when we want to run ROS over a network using multiple systems, all we need to do is change some of these assumptions.

There are 2 main environment variables (ones that you set through the terminal/bash/ssh/etc.) that must be set:

  • ROS_MASTER_URI: information on where the ROS master is located, and how it should be contacted.
  • ROS_IP: Information on the device that is sending data (and where to connect to for other nodes).

In short, these are the commands needed for each variable. They will need to be set for each and every terminal session that is opened (as the environment is flushed on each new session). For example, the addresses "192.168.1.XXX" and "192.168.1.YYY" refer to the master and slave addresses respectively.

This variable need to be set on the master:

export ROS_IP=192.168.1.XXX

These variables need to be set on the slave:

export ROS_IP=192.168.1.YYY
export ROS_MASTER_URI=http://192.168.1.XXX:11311

Common Issues

Here are some common issues you may face when trying to configure and use Distributed ROS. Here we use "node_A" and "node_B" as an example of 2 nodes running on separate PCs. We assume that "node_A" is working as expected, with issues arising when trying to run "node_B".

"ERROR: Unable to communicate with master!" when running "node_B":

  • Most likely the ROS_MASTER_URI has not been correctly set
  • Ensure you typed "http" and not "https"
  • Make sure you have added the port (:11311) to the end
  • If you are using a hostname, make sure you can 'ping', test using IP address instead.

"Couldn't find an AF_INET address for ..." error message in "node_A":

  • Most likely "node_B" that is publishing or subscribing to "node_A" has not configured ROS_IP correctly
  • Make sure ROS_IP matches the IP address of the computer the "node_B" is running on with: echo $ROS_IP
  • If you are running nodes in a Virtual Machine, you may have to use "Bridged Networking" for ROS distributed to work correctly.

"Couldn't find an AF_INET address for ..." error message in "node_B":

  • If your environment doesn't support using a hostname (i.e. you can't ping the other PC just using it's hostname), then you will have to set the do the do the following on each PC: export ROS_HOSTNAME="$(hostname -I)"

Automation

To save some time when using a device that always connects to a separate ROS master, you could add the following commands to the ".bashrc" file. The ROS_IP business will automatically fill in the variable with your IP address (as long as you have a simple connection, and the device is connected when the terminal is opened), and you'll need to replace the IP address in the ROS_MASTER_URI.

export ROS_IP=$(hostname -I)
export ROS_MASTER_URI=http://192.168.1.YYY:11311

Furthermore, for a device that sometimes needs to be set up with Distributed ROS, you could add a bash function to your ".bashrc" file. This will allow you to run the command "kinetic" to automatically configure the ROS master. Running "kinetic" without any arguments will configure the terminal to be set up as the ROS master. Running the command "kinetic 192.168.1.YYY" will configure the terminal to connect to a ROS master on the IP: 192.168.1.YYY.

disros() {
  # Setup for distributed ROS
  export ROS_IP="$(hostname -I)"
  echo "Identifying as: $ROS_IP"
  
  if [ "$#" -eq 1 ]
  then
    export ROS_MASTER_URI="http://$1:11311"
    echo "Connecting to: $ROS_MASTER_URI"
  fi
}
Clone this wiki locally