Robot Operation System (ROS)

The Robot Operating System (ROS) is a middleware which is not an entire Operating System nor a programming langugage, which allows one to handle messaging between components using nodes. These nodes are language agnostic, and ROS itself has a decentralized architecture, with asynchronous communication.

ROS applications should always be in a workspace, and there should be some packages there. To build ROS applications, you will need the official build system for it, called catkin_make, more on the conceptual reasons for its use is described here.

Each ROS application should be in a workspace, for e.g. ~/catkin_ws. From here, the build files can be generated by running catkin_make in the terminal.

Next, add the following at the end of your .bashrc.

source ~/catkin_ws/devel/setup.bash

To show ROS works, we first create create a package, which must be in ~/catkin_wc/src, called ros_tutorial using the catkin_create_pkg command. It takes as arguments the name of the package, followed by the dependencies.

cd ~/catkin_ws/src
catkin_create_pkg ros_tutorial roscpp std_msgs std_srvs

Packages correspond to projects, for e.g. in a course, they can correspond to the assignments. In real life, there could be packages for motion control, state estimation, etc, to be used as building blocks for some application.

We now create a new file called first_cpp_node.cpp inside ~/catkin_ws/src/ros_tutorial/src to show an extremely simple example of running ROS code.

#include <ros/ros.h>

int main(int argc, char** argv) {
    ross::init(argc, argv, "ross_cpp_node"): //initialize ross node with its name, here ross_cpp_node
    ross:NodeHandle nh; // used to define whether this will be a publisher or subscriber

    ROS_INFO("First cpp node created!"); // print a message
    ros::Duration(2.0).sleep(); // wait for 2 seconds
    ROS_INFO("Exited!");

    return 0;
}

Since we are using here C++, we will be making use of CMakeLists.txt in the ros_tutorial directory. Simply uncomment the lines corresponding to the following. Below, add_executable takes as first argument the name of the executable to be created, and the second argument is the source code for the node. The second function links the catkin libraries to the executable.

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

When you make changes to any project, you need to run the makefile in the initial workspace directory.

cd ~/catkin_ws
catkin_make

The output of the make command will tell where the executable is located. Before running this executable however, you need a ROS master. This is often run on a separate terminal.

cd ~/catkin_ws
roscore

Now, the executable we compiled can run.

Now, for the simple example to illustrate publishing and subscribing in nodes. Rewrite the C++ code in first_cpp_node.cpp to the following.

#include <ros/ros.h>

int main(int argc, char** argv) {
    ross::init(argc, argv, "ross_cpp_node"): //initialize ross node with its name, here ross_cpp_node
    ross:NodeHandle nh; // used to define whether this will be a publisher or subscriber

    ROS_INFO("First cpp node created!"); // print a message
    ros::Duration(2.0).sleep(); // wait for 2 seconds

    ros::Rate rate(4); // frequency of running the code

    while(ross:ok()) // while the ROS node is not killed
    {
    ROS_INFO("Welcome!"); // e.g. compute controller, do optimizer etc
    rate.sleep(); 
    }
    ROS_INFO("Exited!");

    return 0;
}

To illustrate the concepts of publishing and subscribing, we now create a small example where the following happens:

For example, a node Odometry can be a publisher, publishes a topic, here for e.g. location, and the subscriber node could be a SLAM node.

Multipler nodes can subscribe to the same topic, however the topic should have the same topic name.

We now create publisher and subscriber nodes as C++ files, with names number_publisher.cpp and number_counter.cpp respectively, in the same directory as first_cpp_node.cpp.

//number_publisher.cpp
#include <ros/ros.h>
#include <std_msgs/Int64.h>

int main (int argc, char **argv)
{
        ros::init(argc, argv, "number_publisher");
        ros::NodeHandle nh;
        ros::Publisher pub = nh.advertise<std_msgs::Int64>("/number", 10); // here /number is the name of the topic
    // putting '/' infront of topics is a convention

        double pub_freq;

        nh.getParam("/pub_freq", pub_freq);

        ros::Rate rate(pub_freq);

        int pub_num;

        nh.getParam("/pub_num", pub_num);

        std_msgs::Int64 msg;
        msg.data = pub_num;

        nh.setParam("/dummy_param", "Bye bye!");
        while (ros::ok()) {                     
                pub.publish(msg);
                ROS_INFO("/number: %ld", msg.data);
                rate.sleep();
        }
        return 0;
}

//number_counter.cpp
#include <ros/ros.h>
#include <std_msgs/Int64.h>
//#include <std_srvs/Trigger.h>

int counter = 0;
ros::Publisher pub;
std_msgs::Int64 new_msg;

void callback_number(const std_msgs::Int64& msg){
        counter += msg.data;    
        new_msg.data = counter;
        ROS_INFO("/number_count: %ld", new_msg.data);
        pub.publish(new_msg);
}

bool callback_reset(std_srvs::Trigger::Request &req, 
                                        std_srvs::Trigger::Response &res)
{//delete
        counter = 0;
        res.success = true;
        res.message = "Counter reset!";
        return true;
}

int main (int argc, char **argv)
{
        ros::init(argc, argv, "number_counter"); //initializing node
        ros::NodeHandle nh;

        pub = nh.advertise<std_msgs::Int64>("/number_count", 10); // this node is both a publisher and subscriber       
        ros::Subscriber sub = nh.subscribe("/number", 1000, callback_number); // once we receive a message from the topic /number, use the callback
        // ros::ServiceServer reset_service = nh.advertiseService("/reset_counter", callback_reset); //delete
        ros::spin(); //makes sure the code doesnt finish after executing
}

Next, remember to modify the make file by adding the following to CMakeList.txt

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

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

Now, to run these files, once again run roscore in a standalone terminal.

roscore

Now, to run the nodes separately, we use the rosrun command, which takes as argument the name of the package, in this case ros_tutorial and the name of the node.

# Run in 2 different terminals
rosrun ros_tutorial number_publisher
rosrun ros_tutorial number_counter

At any point, we can check the status of nodes and topics as well - note that some of them are from the roscore process.

rosnode list
rostopic list

We can ask for information about specific nodes and topics as well.

rosnode info /number_counter
rostopic info /number

For bi-directional communications, there are ROS services, which have both a request and a response. To run a ROS service, uncomment the commented code in number_counter.cpp above, recompile and run the code above, then run the following.

rosservice list
rosservice call /reset_counter

The first command lists the available services, and the second command runs the service that resets the counter. The sum being computed from the number_counter node will reset. More documentation of ROS services are available here. Another possible service could be a case where the client sends 2 numbers, and the server computes their sum and returns it.

To have a good idea what is being done, we can use ROS Bag, which is a tool for recording from and playing back to ROS topics.

Suppose the nodes from above are still running. To record topics, we can do the following, which saves the data in a .bag file in the directory in which the command is run.

rosbag record /number /number_count

We can run the .bag file - since the name of the file is date-time specific we just take the last created file below.

rosbag play $(ls -t |head -n1)

To view the topics of the saved file, run the following in a different terminal, while the above code is being run.

rostopic echo /number_count

There is also a GUI for viewing the ROS bag contents, which can be opened by calling rqt_bag in the terminal.

For the publisher node above, we set the rate and message in the code itself, meaning we have to recompile everytime we change them. We can use ROS params to get or set these values from outside the source code. For example, for the rate, we can add the following code in number_publisher.cpp.

double pub_freq;
nh.getParam("/pub_freq", pub_freq)

We can also check the available parameters at any point.

rosparam list

We can set the parameters from the terminal.

rosparam set /pub_freq 4

However, it can be cumbersome to set parameters manually from the terminal, which is where launch files come into play. A ROS launch file is an XML file with the .launch extension, and defines the parameters, and which nodes to run, so we do not have to open multiple terminal sessions. This can be put in a directory inside the project, and we do not have to recompile everytime it changes. Below is an example of a launch file for the code above, named tutorial_app.launch.

<launch>
    <param name="/pub_freq" type="double" value="3.0" />
    <param name="/pub_num" type="int" value="6" />

    <node name="number_publisher" pkg="ros_tutorial" type="number_publisher" />
    <node name="number_counter" pkg="ros_tutorial" type="number_counter" />
</launch>

We can run the project in one line now, without even starting roscore.

roslaunch ros_tutorial tutorial_app.launch

Emacs 29.4 (Org mode 9.6.15)