Skip to content
pryre edited this page Aug 27, 2017 · 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

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.YYY:11311

To save some time, you could add the following function to the ".bashrc" file. This will allow you to call the command "kinetic" to automatically configure the ROS master, and allow you to call "kinetic 192.168.1.YYY".

kinetic() {
  # Setup for distributed ROS
  export ROS_IP=$(ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | cut -f1  -d'/')
  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