How to Control a Robot’s Velocity Remotely Using ROS

In this tutorial, I will show you how to move a robot around a room remotely, from your own PC. Being able to control a robot’s velocity remotely is useful for a number of use cases, such as building a map, exploring an unknown environment, or getting to hard-to-reach environments. 

By the end of this tutorial, you will be able to send velocity commands to an Arduino microcontroller from your PC. Arduino will then convert those velocity commands into output PWM signals to drive the motors of a two-wheeled differential robot like the one on this post.

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

Set Up the Hardware

This tutorial here shows you how to set up the motors. 

Write the Code

Open the Arduino IDE, and write the following program. The name of my program is motor_controller_diff_drive_2.ino. It builds from the tick_publisher.ino program I wrote in an earlier post.

/*
 * Author: Automatic Addison
 * Website: https://automaticaddison.com
 * Description: ROS node that publishes the accumulated ticks for each wheel
 * (/right_ticks and /left_ticks topics) at regular intervals using the 
 * built-in encoder (forward = positive; reverse = negative). 
 * The node also subscribes to linear & angular velocity commands published on 
 * the /cmd_vel topic to drive the robot accordingly.
 * Reference: Practical Robotics in C++ book (ISBN-10 : 9389423465)
 */

#include <ros.h>
#include <std_msgs/Int16.h>
#include <geometry_msgs/Twist.h>

// Handles startup and shutdown of ROS
ros::NodeHandle nh;

////////////////// Tick Data Publishing Variables and Constants ///////////////

// Encoder output to Arduino Interrupt pin. Tracks the tick count.
#define ENC_IN_LEFT_A 2
#define ENC_IN_RIGHT_A 3

// Other encoder output to Arduino to keep track of wheel direction
// Tracks the direction of rotation.
#define ENC_IN_LEFT_B 4
#define ENC_IN_RIGHT_B 11

// True = Forward; False = Reverse
boolean Direction_left = true;
boolean Direction_right = true;

// Minumum and maximum values for 16-bit integers
// Range of 65,535
const int encoder_minimum = -32768;
const int encoder_maximum = 32767;

// Keep track of the number of wheel ticks
std_msgs::Int16 right_wheel_tick_count;
ros::Publisher rightPub("right_ticks", &right_wheel_tick_count);

std_msgs::Int16 left_wheel_tick_count;
ros::Publisher leftPub("left_ticks", &left_wheel_tick_count);

// Time interval for measurements in milliseconds
const int interval = 30;
long previousMillis = 0;
long currentMillis = 0;

////////////////// Motor Controller Variables and Constants ///////////////////

// Motor A connections
const int enA = 9;
const int in1 = 5;
const int in2 = 6;
 
// Motor B connections
const int enB = 10; 
const int in3 = 7;
const int in4 = 8;

// How much the PWM value can change each cycle
const int PWM_INCREMENT = 1;

// Number of ticks per wheel revolution. We won't use this in this code.
const int TICKS_PER_REVOLUTION = 620;

// Wheel radius in meters
const double WHEEL_RADIUS = 0.033;

// Distance from center of the left tire to the center of the right tire in m
const double WHEEL_BASE = 0.17;

// Number of ticks a wheel makes moving a linear distance of 1 meter
// This value was measured manually.
const double TICKS_PER_METER = 3100; // Originally 2880

// Proportional constant, which was measured by measuring the 
// PWM-Linear Velocity relationship for the robot.
const int K_P = 278;

// Y-intercept for the PWM-Linear Velocity relationship for the robot
const int b = 52;

// Correction multiplier for drift. Chosen through experimentation.
const int DRIFT_MULTIPLIER = 120;

// Turning PWM output (0 = min, 255 = max for PWM values)
const int PWM_TURN = 80;

// Set maximum and minimum limits for the PWM values
const int PWM_MIN = 80; // about 0.1 m/s
const int PWM_MAX = 100; // about 0.172 m/s

// Set linear velocity and PWM variable values for each wheel
double velLeftWheel = 0;
double velRightWheel = 0;
double pwmLeftReq = 0;
double pwmRightReq = 0;

// Record the time that the last velocity command was received
double lastCmdVelReceived = 0;

/////////////////////// Tick Data Publishing Functions ////////////////////////

// Increment the number of ticks
void right_wheel_tick() {
  
  // Read the value for the encoder for the right wheel
  int val = digitalRead(ENC_IN_RIGHT_B);

  if (val == LOW) {
    Direction_right = false; // Reverse
  }
  else {
    Direction_right = true; // Forward
  }
  
  if (Direction_right) {
    
    if (right_wheel_tick_count.data == encoder_maximum) {
      right_wheel_tick_count.data = encoder_minimum;
    }
    else {
      right_wheel_tick_count.data++;  
    }    
  }
  else {
    if (right_wheel_tick_count.data == encoder_minimum) {
      right_wheel_tick_count.data = encoder_maximum;
    }
    else {
      right_wheel_tick_count.data--;  
    }   
  }
}

// Increment the number of ticks
void left_wheel_tick() {
  
  // Read the value for the encoder for the left wheel
  int val = digitalRead(ENC_IN_LEFT_B);

  if (val == LOW) {
    Direction_left = true; // Reverse
  }
  else {
    Direction_left = false; // Forward
  }
  
  if (Direction_left) {
    if (left_wheel_tick_count.data == encoder_maximum) {
      left_wheel_tick_count.data = encoder_minimum;
    }
    else {
      left_wheel_tick_count.data++;  
    }  
  }
  else {
    if (left_wheel_tick_count.data == encoder_minimum) {
      left_wheel_tick_count.data = encoder_maximum;
    }
    else {
      left_wheel_tick_count.data--;  
    }   
  }
}

/////////////////////// Motor Controller Functions ////////////////////////////

// Calculate the left wheel linear velocity in m/s every time a 
// tick count message is rpublished on the /left_ticks topic. 
void calc_vel_left_wheel(){
  
  // Previous timestamp
  static double prevTime = 0;
  
  // Variable gets created and initialized the first time a function is called.
  static int prevLeftCount = 0;

  // Manage rollover and rollunder when we get outside the 16-bit integer range 
  int numOfTicks = (65535 + left_wheel_tick_count.data - prevLeftCount) % 65535;

  // If we have had a big jump, it means the tick count has rolled over.
  if (numOfTicks > 10000) {
        numOfTicks = 0 - (65535 - numOfTicks);
  }

  // Calculate wheel velocity in meters per second
  velLeftWheel = numOfTicks/TICKS_PER_METER/((millis()/1000)-prevTime);

  // Keep track of the previous tick count
  prevLeftCount = left_wheel_tick_count.data;

  // Update the timestamp
  prevTime = (millis()/1000);

}

// Calculate the right wheel linear velocity in m/s every time a 
// tick count message is published on the /right_ticks topic. 
void calc_vel_right_wheel(){
  
  // Previous timestamp
  static double prevTime = 0;
  
  // Variable gets created and initialized the first time a function is called.
  static int prevRightCount = 0;

  // Manage rollover and rollunder when we get outside the 16-bit integer range 
  int numOfTicks = (65535 + right_wheel_tick_count.data - prevRightCount) % 65535;

  if (numOfTicks > 10000) {
        numOfTicks = 0 - (65535 - numOfTicks);
  }

  // Calculate wheel velocity in meters per second
  velRightWheel = numOfTicks/TICKS_PER_METER/((millis()/1000)-prevTime);

  prevRightCount = right_wheel_tick_count.data;
  
  prevTime = (millis()/1000);

}

// Take the velocity command as input and calculate the PWM values.
void calc_pwm_values(const geometry_msgs::Twist& cmdVel) {
  
  // Record timestamp of last velocity command received
  lastCmdVelReceived = (millis()/1000);
  
  // Calculate the PWM value given the desired velocity 
  pwmLeftReq = K_P * cmdVel.linear.x + b;
  pwmRightReq = K_P * cmdVel.linear.x + b;

  // Check if we need to turn 
  if (cmdVel.angular.z != 0.0) {

    // Turn left
    if (cmdVel.angular.z > 0.0) {
      pwmLeftReq = -PWM_TURN;
      pwmRightReq = PWM_TURN;
    }
    // Turn right    
    else {
      pwmLeftReq = PWM_TURN;
      pwmRightReq = -PWM_TURN;
    }
  }
  // Go straight
  else {
    
    // Remove any differences in wheel velocities 
    // to make sure the robot goes straight
    static double prevDiff = 0;
    static double prevPrevDiff = 0;
    double currDifference = velLeftWheel - velRightWheel; 
    double avgDifference = (prevDiff+prevPrevDiff+currDifference)/3;
    prevPrevDiff = prevDiff;
    prevDiff = currDifference;

    // Correct PWM values of both wheels to make the vehicle go straight
    pwmLeftReq -= (int)(avgDifference * DRIFT_MULTIPLIER);
    pwmRightReq += (int)(avgDifference * DRIFT_MULTIPLIER);
  }

  // Handle low PWM values
  if (abs(pwmLeftReq) < PWM_MIN) {
    pwmLeftReq = 0;
  }
  if (abs(pwmRightReq) < PWM_MIN) {
    pwmRightReq = 0;  
  }  
}

void set_pwm_values() {

  // These variables will hold our desired PWM values
  static int pwmLeftOut = 0;
  static int pwmRightOut = 0;

  // If the required PWM is of opposite sign as the output PWM, we want to
  // stop the car before switching direction
  static bool stopped = false;
  if ((pwmLeftReq * velLeftWheel < 0 && pwmLeftOut != 0) ||
      (pwmRightReq * velRightWheel < 0 && pwmRightOut != 0)) {
    pwmLeftReq = 0;
    pwmRightReq = 0;
  }

  // Set the direction of the motors
  if (pwmLeftReq > 0) { // Left wheel forward
    digitalWrite(in1, HIGH);
    digitalWrite(in2, LOW);
  }
  else if (pwmLeftReq < 0) { // Left wheel reverse
    digitalWrite(in1, LOW);
    digitalWrite(in2, HIGH);
  }
  else if (pwmLeftReq == 0 && pwmLeftOut == 0 ) { // Left wheel stop
    digitalWrite(in1, LOW);
    digitalWrite(in2, LOW);
  }
  else { // Left wheel stop
    digitalWrite(in1, LOW);
    digitalWrite(in2, LOW); 
  }

  if (pwmRightReq > 0) { // Right wheel forward
    digitalWrite(in3, HIGH);
    digitalWrite(in4, LOW);
  }
  else if(pwmRightReq < 0) { // Right wheel reverse
    digitalWrite(in3, LOW);
    digitalWrite(in4, HIGH);
  }
  else if (pwmRightReq == 0 && pwmRightOut == 0) { // Right wheel stop
    digitalWrite(in3, LOW);
    digitalWrite(in4, LOW);
  }
  else { // Right wheel stop
    digitalWrite(in3, LOW);
    digitalWrite(in4, LOW); 
  }

  // Increase the required PWM if the robot is not moving
  if (pwmLeftReq != 0 && velLeftWheel == 0) {
    pwmLeftReq *= 1.5;
  }
  if (pwmRightReq != 0 && velRightWheel == 0) {
    pwmRightReq *= 1.5;
  }

  // Calculate the output PWM value by making slow changes to the current value
  if (abs(pwmLeftReq) > pwmLeftOut) {
    pwmLeftOut += PWM_INCREMENT;
  }
  else if (abs(pwmLeftReq) < pwmLeftOut) {
    pwmLeftOut -= PWM_INCREMENT;
  }
  else{}
  
  if (abs(pwmRightReq) > pwmRightOut) {
    pwmRightOut += PWM_INCREMENT;
  }
  else if(abs(pwmRightReq) < pwmRightOut) {
    pwmRightOut -= PWM_INCREMENT;
  }
  else{}

  // Conditional operator to limit PWM output at the maximum 
  pwmLeftOut = (pwmLeftOut > PWM_MAX) ? PWM_MAX : pwmLeftOut;
  pwmRightOut = (pwmRightOut > PWM_MAX) ? PWM_MAX : pwmRightOut;

  // PWM output cannot be less than 0
  pwmLeftOut = (pwmLeftOut < 0) ? 0 : pwmLeftOut;
  pwmRightOut = (pwmRightOut < 0) ? 0 : pwmRightOut;

  // Set the PWM value on the pins
  analogWrite(enA, pwmLeftOut); 
  analogWrite(enB, pwmRightOut); 
}

// Set up ROS subscriber to the velocity command
ros::Subscriber<geometry_msgs::Twist> subCmdVel("cmd_vel", &calc_pwm_values );

void setup() {

  // Set pin states of the encoder
  pinMode(ENC_IN_LEFT_A , INPUT_PULLUP);
  pinMode(ENC_IN_LEFT_B , INPUT);
  pinMode(ENC_IN_RIGHT_A , INPUT_PULLUP);
  pinMode(ENC_IN_RIGHT_B , INPUT);

  // Every time the pin goes high, this is a tick
  attachInterrupt(digitalPinToInterrupt(ENC_IN_LEFT_A), left_wheel_tick, RISING);
  attachInterrupt(digitalPinToInterrupt(ENC_IN_RIGHT_A), right_wheel_tick, RISING);
  
  // Motor control pins are outputs
  pinMode(enA, OUTPUT);
  pinMode(enB, OUTPUT);
  pinMode(in1, OUTPUT);
  pinMode(in2, OUTPUT);
  pinMode(in3, OUTPUT);
  pinMode(in4, OUTPUT);
 
  // Turn off motors - Initial state
  digitalWrite(in1, LOW);
  digitalWrite(in2, LOW);
  digitalWrite(in3, LOW);
  digitalWrite(in4, LOW);
 
  // Set the motor speed
  analogWrite(enA, 0); 
  analogWrite(enB, 0);

  // ROS Setup
  nh.getHardware()->setBaud(115200);
  nh.initNode();
  nh.advertise(rightPub);
  nh.advertise(leftPub);
  nh.subscribe(subCmdVel);
}

void loop() {
  
  nh.spinOnce();
  
  // Record the time
  currentMillis = millis();

  // If the time interval has passed, publish the number of ticks,
  // and calculate the velocities.
  if (currentMillis - previousMillis > interval) {
    
    previousMillis = currentMillis;

    // Publish tick counts to topics
    leftPub.publish( &left_wheel_tick_count );
    rightPub.publish( &right_wheel_tick_count );

    // Calculate the velocity of the right and left wheels
    calc_vel_right_wheel();
    calc_vel_left_wheel();
    
  }
  
  // Stop the car if there are no cmd_vel messages
  if((millis()/1000) - lastCmdVelReceived > 1) {
    pwmLeftReq = 0;
    pwmRightReq = 0;
  }

  set_pwm_values();
}

Upload the program to the Arduino. You might see a warning about low memory. Just ignore it.

Note that I set the minimum PWM value (i.e. motor speed) to 80 and the maximum to 100. These numbers might be different if you use other motors.

80 is really the ideal speed when you want to map an indoor environment (found through testing). The actual PWM range is 0 to 255, but we won’t want to get to either of these extremes.

The number of ticks per 1 full revolution of a wheel is 620.

Through testing I also found the following PWM value to speed (m/s relationships)

  • 80 = 0.1 m/s
  • 100 = 0.172 m/s

This yields an equation of:

PWM_Value = 277.78 * (Speed in m/s) + 52.22

Launch the Motor Controller

I will use the rqt_robot_steering interface that I used in this tutorial to publish Twist messages to the cmd_vel topic. A Twist message in ROS is a special kind of topic that consists of the x, y, and z components of the linear and angular velocity. 

The only two velocity components we will use in this case is the linear velocity along the x-axis and the angular velocity around the z-axis. If you are not familiar with how 3D axes work in robotics, take a look at this post.

Arduino will read Twist messages that are published on the /cmd_vel topic.

Make sure your Arduino is plugged into the Jetson Nano.

Turn on your Jetson Nano.

Turn on the batteries for the motor. 

Let’s test our motor controller.

Open a new terminal window, and launch ROS.

cd ~/catkin_ws
roscore

Open a new terminal window, and launch the ROS serial server. 

rosrun rosserial_python serial_node.py _port:=/dev/ttyACM0 _baud:=115200

Open a new terminal window, and launch the rqt_robot_steering node so that we can send velocity commands to Arduino.

rosrun rqt_robot_steering rqt_robot_steering

Open a new terminal window, and check out the active topics.

rostopic list

Let’s check out the /cmd_vel topic.

rostopic echo /cmd_vel

Move the slider on the rqt_robot_steering GUI, and watch the /cmd_vel velocity commands change accordingly. Your robot should move accordingly.

2021-05-17-16.37.41

Check out the rqt graph. Open a new terminal window, and type:

rosrun rqt_graph rqt_graph
2021-05-17-15.39.46

Run a Launch File (Optional)

Now, let’s add the rqt_robot_steering node to our mother launch file named jetson_nanobot.launch.

Go to the launch file.

roscd launch_jetson_nano_bot
cd launch
gedit  jetson_nanobot.launch

Add the following lines below the Lidar data publisher node.

  <node pkg="rqt_robot_steering" type="rqt_robot_steering" name="rqt_robot_steering">
  </node>

Save the file, and close it.

Open a new terminal window, and go to the home directory.

cd

Launch the launch file.

roslaunch launch_jetson_nano_bot jetson_nanobot.launch

If you open a new terminal window, and type the following command, you will be able to see the active topics.

rostopic list

That’s it! Keep building.

How to Publish LIDAR Data Using a ROS Launch File

In this tutorial, I will show you how to publish LIDAR data using a ROS launch file.

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

Getting Started

To publish LIDAR data, you are going to want to follow this tutorial. Follow all the instructions up until you reach the section entitled “Build a Map Using the Hector-SLAM ROS Package”. Don’t go beyond that point.

Create the Launch File

Once you’ve completed the step above, open a terminal window, and go to the main launch file.

Turn on your Jetson Nano.

Open a terminal window.

Go to your launch file.

roscd launch_jetson_nano_bot
cd launch
gedit jetson_nanobot.launch

Add the following lines below the IMU data publisher node.

  <!-- Lidar Data Publisher Using RPLIDAR from Slamtec -->
  <!-- Used for obstacle avoidance and can be used for mapping --> 
  <!-- Publish: /scan -->
  <node name="rplidarNode"          pkg="rplidar_ros"  type="rplidarNode" output="screen">
    <param name="serial_port"         type="string" value="/dev/ttyUSB0"/>
    <param name="serial_baudrate"     type="int"    value="115200"/><!--A1/A2 -->
    <!--param name="serial_baudrate"  type="int"    value="256000"--><!--A3 -->
    <param name="frame_id"            type="string" value="laser"/>
    <param name="inverted"            type="bool"   value="false"/>
    <param name="angle_compensate"    type="bool"   value="true"/>
  </node>  

Save the file, and close it.

Launch the Launch File

Open a new terminal window, and go to the home directory.

cd

Launch the launch file.

roslaunch launch_jetson_nano_bot jetson_nanobot.launch

If you open a new terminal window, and type the following command, you will be able to see the active topics.

rostopic list

Let’s see what LIDAR data is being published to the /scan ROS topic. Open a new terminal window, and type the following command to see a single message (the number after the -n tag indicates how many messages you want to see):

rostopic echo -n1 /scan

Alternatively, you can see the full stream of messages by typing:

rostopic echo /scan

Press CTRL + C to stop the data.

Visualize the Data

Open a new terminal window, and start rviz so we can visualize the data.

rosrun rviz rviz

Change the Fixed Frame to laser.

Click the Add button in the bottom-left of the window.

Select LaserScan, and click OK.

Set the LaserScan topic to /scan.

Increase the size to 0.03 so that you can see the data more easily.

Here is what you should see:

2021-05-12-21.37.57

Press CTRL + C on all windows to shut everything down.

How to Publish IMU Data Using ROS and the BNO055 IMU Sensor

In this tutorial, I will show you how to use the BNO055 sensor (using i2c communication) to generate IMU data for a ROS-based robot. 

I actually prefer the BNO055 over other IMU sensors like the MPU6050. I have found that the BNO055 generates more accurate and less noisy data than the MPU6050.

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

2021-06-10-18.15.05

You Will Need

You will need the following components (#ad).

Disclosure (#ad): As an Amazon Associate I earn from qualifying purchases.

Set Up the Hardware

First, let’s set up the hardware. Remember you need to solder the pins to the BNO055.

Make the following connections between the BNO055 and the NVIDIA Jetson Nano:

  • Connect BNO055 Vin to pin 17 (3.3V) of the Jetson Nano.
  • Connect BNO055 GND to pin 25 (GND) of the Jetson Nano.
  • Connect BNO055 SDA to pin 3 (SDA) of the Jetson Nano.
  • Connect BNO055 SCL to pin 5 (SCL) of the Jetson Nano.
1-pin-diagram-nvidia-jetson-nano

Set Up the Communication Protocol

Our BNO055 will use the I2C serial communication protocol. What this means is that data will be transferred from the IMU to the Jetson Nano one bit at a time.

Turn on your Jetson Nano.

Open a terminal window.

Type the following command to verify that you can see the BNO055.

sudo i2cdetect -r -y 1

Check that the bus of the BNO055 has an address of 0x28.

2021-05-24-13.13.22

Install and Build the BNO055 ROS Package

Let’s get all the software set up.

Turn on your Jetson Nano.

Open a new terminal window.

Install the libi2c-dev library.

sudo apt-get update
sudo apt-get install libi2c-dev

Install the BNO055 ROS package.

cd ~/catkin_ws/src

Remove any existing package named ‘ros_imu_bno055’.

git clone https://github.com/dheera/ros-imu-bno055.git

Build the package.

cd ~/catkin_ws/
catkin_make --only-pkg-with-deps imu_bno055

Open a new terminal window, and see if our package depends on other packages, type these commands. Ignore any error messages you see in the terminal:

rosdep update
rosdep check <package_name>

We will do:

rosdep check imu_bno055

You should see a message that says “All system dependencies have been satisfied.”

Reboot your computer.

sudo reboot

View the IMU Data

Install the ROS IMU plugin so we can visualize the IMU data on rviz.

sudo apt-get install ros-melodic-rviz-imu-plugin

Open a terminal window, and type:

roslaunch imu_bno055 imu.launch

Open a terminal window and see the active topics.

rostopic list
active_topics

Let’s see the imu data on the topic named /imu/data.

rostopic echo /imu/data
rostopic_echo_imu_messages

If rviz has not already launched, open a new terminal and launch rviz.

rviz

Change the fixed frame to imu.

Click the Add button in the bottom left.

Click imu under rviz_imu_plugin

Click OK.

Change the topic of the imu to /imu/data.

Move your BNO055 around, and you will see the axes move. The red line is the x-axis, the green line is the y-axis, and the blue line is the z-axis.

rviz_axes

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

Update Your Launch File (Optional)

I have a launch file I created in this post.

I will add the following lines to my launch file (which is in XML format).

  <!-- IMU Data Publisher Using the BNO055 IMU Sensor -->
  <!-- Publish: /imu/data -->
  <node ns="imu" name="imu_node" pkg="imu_bno055" type="bno055_i2c_node" respawn="true" respawn_delay="2">
    <param name="device" type="string" value="/dev/i2c-1"/>
    <param name="address" type="int" value="40"/> <!-- 0x28 == 40 is the default for BNO055 -->
    <param name="frame_id" type="string" value="imu"/>
  </node>

That’s it. Keep building!