How to Build a Simulated Mobile Robot Base Using ROS

In this tutorial, we will build a mobile robot base from scratch using ROS. In a future post, I will add a robotic arm to this base so that we have a complete mobile manipulator. By the end of this post, you will have a robot that looks like this:

6-robot-base-gif

This tutorial would not have been possible without Ramkumar Gandhinathan and Lentin Joseph’s awesome book ROS Robotics Projects Second Edition (Disclosure: As an Amazon Associate I earn from qualifying purchases). I highly recommend it if you want to learn ROS 1. Many of the files (URDF, configuration, and STL files), come from their book’s public GitHub page.

Real-World Applications

This project has a number of real-world applications: 

  • Indoor Delivery Robots
  • Order Fulfillment
  • Factories
  • Warehouses
  • Space Exploration
  • Power Plants

Let’s get started!

Prerequisites

Install ROS Packages

Let’s begin by installing some packages that we will need to accomplish our objective.

sudo apt-get install ros-noetic-ros-control
sudo apt-get install ros-noetic-ros-controllers
sudo apt-get install ros-noetic-gazebo-ros-control

Create a ROS Package

Create a ROS package.

In a new terminal window, move to the src (source) folder of your workspace.

cd ~/catkin_ws/src

Now create the package.

catkin_create_pkg mobile_manipulator_body std_msgs roscpp rospy
cd ~/catkin_ws/
catkin_make --only-pkg-with-deps mobile_manipulator_body

Create Folders

Open a new terminal window.

Move to your package.

roscd mobile_manipulator_body

Create these four folders.

mkdir config launch meshes urdf

Build the Base of the Robot

Now move to your meshes folder.

cd meshes

Go to this link, and download all the mesh files. 

Put the mesh files into your meshes folder inside your mobile_manipulator_body package.

Check to see all the files are in there.

dir
1-dir-mesh-files

Move to the urdf folder.

cd ..
cd urdf

Create a file named robot_base.urdf. In this file, we will define the four wheels of the robot and the base (i.e. five different “links”. Links are the rigid parts of the robot).

gedit robot_base.urdf

Copy this code for robot_base.urdf into that file. 

Save and close the file.

Now, let’s launch RViz to see what our robot looks like so far.

roscd mobile_manipulator_body/urdf/
roslaunch urdf_tutorial display.launch model:=robot_base.urdf
2-robot-base

Move the wheels using the sliders. 

3-gui

Press CTRL + C in all open terminal windows to close everything down.

Now, let’s set up the configuration parameters for the controllers.

Open a new terminal window.

Go to the config file of your package.

roscd mobile_manipulator_body/config/

Now create a file named control.yaml.

gedit control.yaml

Add the control.yaml code inside there.

Save and close the file.

Launch the Base of the Robot

Now let’s launch the base of the robot.

Open a new terminal window, and go to the package.

roscd mobile_manipulator_body/launch/

Create a new launch file.

gedit base_gazebo_control.launch

Add the code for base_gazebo_control.launch inside there.

Save and close the file.

Now let’s launch the robot in Gazebo.

Open a new terminal window.

Move to your catkin workspace.

cd ~/catkin_ws/
roslaunch mobile_manipulator_body base_gazebo_control.launch

Here is how the robot looks.

4-robot-base

Here are the active ROS topics.

rostopic list

You can steer the robot by opening a new window and typing:

rosrun rqt_robot_steering rqt_robot_steering

You will need to change the topic inside the GUI to:

/robot_base_velocity_controller/cmd_vel

6-rqt-steering

To see the velocity messages, open a new window and type:

rostopic echo /robot_base_velocity_controller/cmd_vel
7-ros-topic-list-velocity-cmd

References

ROS Robotics Projects Second Edition

What is an Occupancy Grid Map?

In this tutorial, I will teach you what an occupancy grid map is. If you work in ROS long enough, you will eventually learn how to build an occupancy grid map

In this post, I built an occupancy grid map from scratch to enable a robot to navigate safely around a room.

Real-World Applications

This project has a number of real-world applications: 

  • Indoor Delivery Robots
  • Mapping of Underground Mines, Caves, and Hard-to-Reach Environments
  • Robot Vacuums
  • Order Fulfillment
  • Factories

What is a Grid Map?

Imagine you want to create a robot to navigate across a factory floor. In order to navigate from one point to another with precision, the robot needs to have a map of the floor. We can represent the factory floor as a grid composed of, for example, 1 meter x 1 meter cells. The grid has a horizontal axis (i.e. x axis) and a vertical axis ( y axis).

The image below is an example grid map of a factory floor. The big objects within the grid are obstacles (e.g. shelves). 

1-grid-mapJPG
An overhead view of a factory floor represented abstractly as a grid map with 1 meter x 1 meter cells.

The cool thing about a grid map is that we can determine what is in each cell by looking up the coordinate. For example, we can see in the image above that a shelf is located at (x=6, y=8). Therefore, that cell is occupied. However, open factory floor is located at (x=3, y=3). That cell is not occupied.

We can use a grid map to abstractly represent any indoor environment, including a house, apartment, and office. A robot’s position in the environment at any given time is relative to the corner of the map (x=0, y=0). 

Knowing what part of a factory floor is open space and what part of a factory floor contains obstacles helps a robot properly plan the shortest, collision-free path from one point to another.

One other thing we need to keep in mind is that I assumed the map above has 1 meter spacing between each grid cell. For example, let’s say a robot’s location in the real world is recorded as (3.5, 4.3). On the grid cell, this location would correspond to cell (x=3, y=4) because the grid map is 1 meter resolution. 

But what if we wanted to change the map resolution to 0.1 meter spacing between each grid cell? Let’s suppose the robot reported its location as (3.5, 4.3). What would the corresponding location be on the grid map?

(3.5 * (1 cell/0.1 meters), 4.3 * (1 cell/0.1 meters)) = (35, 43)

Thus, for a 0.1 resolution grid map, a robot that reports its position as (3.5, 4.3) corresponds to a grid map location of (35, 43).  

What is an Occupancy Grid Map?

In an occupancy grid map, each cell is marked with a number that indicates the likelihood the cell contains an object. The number is often 0 (free space) to 100 (100% likely occupied). Unscanned areas (i.e. by the LIDAR, ultrasonic sensor, or some other object detection sensor) would be marked -1.

For example, consider the map below.

2-grid-mapJPG

An occupancy grid map might look like the image below. Note the robot is in blue, and the LIDAR is the red square. The black lines are laser beams.

3-occupancy-grid-mapJPG

That’s it. Keep building!

How To Send Goals to the ROS Navigation Stack Using C++

In my previous post on the ROS Navigation Stack, when we wanted to give our robot a goal location, we used the RViz graphical user interface. However, if you want to send goals to the ROS Navigation Stack using code, you can do that too. We’ll use C++.

The official tutorial is on this page, but I will walk you through all the steps below.

Real-World Applications

This project has a number of real-world applications: 

  • Indoor Delivery Robots
  • Room Service Robots
  • Mapping of Underground Mines, Caves, and Hard-to-Reach Environments
  • Robot Vacuums
  • Order Fulfillment
  • Factories

Prerequisites

  • You have a robot that is running the ROS Navigation Stack. I will be continuing from this tutorial.
2021-06-02-08.51.37

Determine the Coordinates of the Goal Locations

Open a new terminal window, and launch the launch file.

roslaunch navstack_pub jetson_nano_bot.launch

Make a note of the X and Y coordinates of each desired goal location. I use RViz Point Publish button to accomplish this. When you click that button, you can see the coordinate values by typing the following command in a terminal:

ros topic echo /clicked_point 

I want to have an X, Y coordinate for the following six goal locations in my apartment.

1 = Bathroom

2 = Bedroom

3 = Front Door

4 = Living Room

5 = Home Office

6 = Kitchen

You notice how I numbered the goal locations above. That was intentional. I want to be able to type a number into a terminal window and have the robot navigate to that location.

For example, if I type 6, the robot will move to the kitchen. If I type 2, the robot will go to my bedroom.

2021-06-02-08.53.34

Write the Code

Now let’s write some C++.

Open a terminal window.

roscd navstack_pub
cd src
gedit send_goals.cpp
/*
 * Author: Automatic Addison
 * Date: May 30, 2021
 * ROS Version: ROS 1 - Melodic
 * Website: https://automaticaddison.com
 * This ROS node sends the robot goals to move to a particular location on 
 * a map. I have configured this program to the map of my own apartment.
 *
 * 1 = Bathroom
 * 2 = Bedroom
 * 3 = Front Door
 * 4 = Living Room
 * 5 = Home Office
 * 6 = Kitchen (Default)
 */

#include <ros/ros.h>
#include <move_base_msgs/MoveBaseAction.h>
#include <actionlib/client/simple_action_client.h>
#include <iostream>

using namespace std;

// Action specification for move_base
typedef actionlib::SimpleActionClient<move_base_msgs::MoveBaseAction> MoveBaseClient;

int main(int argc, char** argv){
  
  // Connect to ROS
  ros::init(argc, argv, "simple_navigation_goals");

  //tell the action client that we want to spin a thread by default
  MoveBaseClient ac("move_base", true);

  // Wait for the action server to come up so that we can begin processing goals.
  while(!ac.waitForServer(ros::Duration(5.0))){
    ROS_INFO("Waiting for the move_base action server to come up");
  }

  int user_choice = 6;
  char choice_to_continue = 'Y';
  bool run = true;
	
  while(run) {

    // Ask the user where he wants the robot to go?
    cout << "\nWhere do you want the robot to go?" << endl;
    cout << "\n1 = Bathroom" << endl;
    cout << "2 = Bedroom" << endl;
    cout << "3 = Front Door" << endl;
    cout << "4 = Living Room" << endl;
    cout << "5 = Home Office" << endl;
    cout << "6 = Kitchen" << endl;
    cout << "\nEnter a number: ";
    cin >> user_choice;

    // Create a new goal to send to move_base 
    move_base_msgs::MoveBaseGoal goal;

    // Send a goal to the robot
    goal.target_pose.header.frame_id = "map";
    goal.target_pose.header.stamp = ros::Time::now();
		
    bool valid_selection = true;

    // Use map_server to load the map of the environment on the /map topic. 
    // Launch RViz and click the Publish Point button in RViz to 
    // display the coordinates to the /clicked_point topic.
    switch (user_choice) {
      case 1:
        cout << "\nGoal Location: Bathroom\n" << endl;
        goal.target_pose.pose.position.x = 10.0;
	goal.target_pose.pose.position.y = 3.7;
        goal.target_pose.pose.orientation.w = 1.0;
        break;
      case 2:
        cout << "\nGoal Location: Bedroom\n" << endl;
        goal.target_pose.pose.position.x = 8.1;
	goal.target_pose.pose.position.y = 4.3;
        goal.target_pose.pose.orientation.w = 1.0;
        break;
      case 3:
        cout << "\nGoal Location: Front Door\n" << endl;
        goal.target_pose.pose.position.x = 10.5;
	goal.target_pose.pose.position.y = 2.0;
        goal.target_pose.pose.orientation.w = 1.0;
        break;
      case 4:
        cout << "\nGoal Location: Living Room\n" << endl;
        goal.target_pose.pose.position.x = 5.3;
	goal.target_pose.pose.position.y = 2.7;
        goal.target_pose.pose.orientation.w = 1.0;
        break;
      case 5:
        cout << "\nGoal Location: Home Office\n" << endl;
        goal.target_pose.pose.position.x = 2.5;
	goal.target_pose.pose.position.y = 2.0;
        goal.target_pose.pose.orientation.w = 1.0;
        break;
      case 6:
        cout << "\nGoal Location: Kitchen\n" << endl;
        goal.target_pose.pose.position.x = 3.0;
	goal.target_pose.pose.position.y = 6.0;
        goal.target_pose.pose.orientation.w = 1.0;
        break;
      default:
        cout << "\nInvalid selection. Please try again.\n" << endl;
        valid_selection = false;
    }		
		
    // Go back to beginning if the selection is invalid.
    if(!valid_selection) {
      continue;
    }

    ROS_INFO("Sending goal");
    ac.sendGoal(goal);

    // Wait until the robot reaches the goal
    ac.waitForResult();

    if(ac.getState() == actionlib::SimpleClientGoalState::SUCCEEDED)
      ROS_INFO("The robot has arrived at the goal location");
    else
      ROS_INFO("The robot failed to reach the goal location for some reason");
		
    // Ask the user if he wants to continue giving goals
    do {
      cout << "\nWould you like to go to another destination? (Y/N)" << endl;
      cin >> choice_to_continue;
      choice_to_continue = tolower(choice_to_continue); // Put your letter to its lower case
    } while (choice_to_continue != 'n' && choice_to_continue != 'y'); 

    if(choice_to_continue =='n') {
        run = false;
    }  
  }
  
  return 0;
}

Save and close the file.

Now open a new terminal window, and type the following command:

roscd navstack_pub
gedit CMakeLists.txt

Go to the bottom of the file.

Add the following lines.

INCLUDE_DIRECTORIES(/usr/local/lib)
LINK_DIRECTORIES(/usr/local/lib)

add_executable(send_goals src/send_goals.cpp)
target_link_libraries(send_goals ${catkin_LIBRARIES})
cd ~/catkin_ws/

Compile the package.

catkin_make --only-pkg-with-deps navstack_pub

Open a new terminal to launch the nodes.

roslaunch navstack_pub jetson_nano_bot.launch

Open another terminal to launch the send_goals node.

rosrun navstack_pub send_goals
send-goals-1

Follow the prompt to send your first goal to the ROS Navigation Stack.

send-goals-3
send-goals-4-1
send-goals-5

To see the node graph (which shows what ROS nodes are running to make all this magic happen), type:

rqt_graph