Exploring ROS with a 2 wheeled robot #8 – Bug 0

Exploring ROS with a 2 wheeled robot #8 – Bug 0

Work with the Bug 0 algorithm

In this post, we are creating a new script to implement the Bug 0 behavior. This one that will drive our robot to a give position, switching between “go to point” and “wall follower” algorithms.


Step 1 – Introduction

First, let’s check how it is going to work, in a top-down view, we show the proposed architecture below:

We are launching 3 different nodes: Bug 0Go to point and Wall follower.

For the 2 nodes we have created before, we will adjust (create the switch service) in order to make them work together. Let’s go to code!


Step 2 – Go-to-point adjustments

I start here from the Go to point node, let’s check what we have to change!

We need to import a library for the service we are about to create:

from std_srvs.srv import *

And create a global variable to switch it to ON and OFF (starting by OFF)

active_ = False

The desired position is not anymore a fixed position, instead it comes from ROS parameter server:

desired_position_.x = rospy.get_param('des_pos_x')
desired_position_.y = rospy.get_param('des_pos_y')

Create a function to receive the service call:

# service callback
def go_to_point_switch(req):
    global active_
    active_ = req.data
    res = SetBoolResponse()
    res.success = True
    res.message = 'Done!'
    return res

And the main function is going to be changed a bit.
It’s basically considering the variable active_ before following its original rules.
Here it goes the code:

def main():
    global pub, active_
    
    rospy.init_node('go_to_point')
    pub = rospy.Publisher('/cmd_vel', Twist, queue_size=1)
    sub_odom = rospy.Subscriber('/odom', Odometry, clbk_odom)
    srv = rospy.Service('go_to_point_switch', SetBool, go_to_point_switch)
    
    rate = rospy.Rate(20)
    while not rospy.is_shutdown():
        if not active_:
            continue
        else:
            if state_ == 0:
                fix_yaw(desired_position_)
            elif state_ == 1:
                go_straight_ahead(desired_position_)
            elif state_ == 2:
                done()
            else:
                rospy.logerr('Unknown state!')
        
        rate.sleep()

Step 3 – Wall follower adjustments

For the wall follower, we have to do some similar modifications, let’s check:

Importing services library:

from std_srvs.srv import *

And create a global variable to switch it to ON and OFF (starting by OFF), as well:

active_ = False

Define the function to be called when the service is requested:

def wall_follower_switch(req):
    global active_
    active_ = req.data
    res = SetBoolResponse()
    res.success = True
    res.message = 'Done!'
    return res

And the main function must, also, take into account the _active global variable before publishing to the robot differential drive:

def main():
    global pub_, active_
    
    rospy.init_node('wall_follower')
    pub_ = rospy.Publisher('/cmd_vel', Twist, queue_size=1)
    sub = rospy.Subscriber('/m2wr/laser/scan', LaserScan, clbk_laser)
    srv = rospy.Service('wall_follower_switch', SetBool, wall_follower_switch)
    
    rate = rospy.Rate(20)
    while not rospy.is_shutdown():
        if not active_:
            rate.sleep()
            continue
        
        msg = Twist()
        if state_ == 0:
            msg = find_wall()
        elif state_ == 1:
            msg = turn_left()
        elif state_ == 2:
            msg = follow_the_wall()
            pass
        else:
            rospy.logerr('Unknown state!')
        
        pub_.publish(msg)
        
        rate.sleep()

Step 4 – Bug 0 – The Manager Node

We are done with our previous created nodes, now let’s create a new one to manage them:

Let’s start creating a new file ~/catkin_ws/src/motion_plan/scripts/bug0.py

First, let’s import the libraries:

#! /usr/bin/env python

# import ros stuff
import rospy
# import ros message
from geometry_msgs.msg import Point
from sensor_msgs.msg import LaserScan
from nav_msgs.msg import Odometry
from tf import transformations
# import ros service
from std_srvs.srv import *

import math

Then, we define some global variables, in charge of controlling the flow of the script:

srv_client_go_to_point_ = None
srv_client_wall_follower_ = None
yaw_ = 0
yaw_error_allowed_ = 5 * (math.pi / 180) # 5 degrees
position_ = Point()
desired_position_ = Point()
desired_position_.x = rospy.get_param('des_pos_x')
desired_position_.y = rospy.get_param('des_pos_y')
desired_position_.z = 0
regions_ = None
state_desc_ = ['Go to point', 'wall following']
state_ = 0
# 0 - go to point
# 1 - wall following

Now, let’s define the callback methods (laser and odometry subscribers):

# callbacks
def clbk_odom(msg):
    global position_, yaw_
    
    # position
    position_ = msg.pose.pose.position
    
    # yaw
    quaternion = (
        msg.pose.pose.orientation.x,
        msg.pose.pose.orientation.y,
        msg.pose.pose.orientation.z,
        msg.pose.pose.orientation.w)
    euler = transformations.euler_from_quaternion(quaternion)
    yaw_ = euler[2]

def clbk_laser(msg):
    global regions_
    regions_ = {
        'right':  min(min(msg.ranges[0:143]), 10),
        'fright': min(min(msg.ranges[144:287]), 10),
        'front':  min(min(msg.ranges[288:431]), 10),
        'fleft':  min(min(msg.ranges[432:575]), 10),
        'left':   min(min(msg.ranges[576:719]), 10),
    }

Two other functions are created, in order to help us switching between states and calculate normal angles:

def change_state(state):
    global state_, state_desc_
    global srv_client_wall_follower_, srv_client_go_to_point_
    state_ = state
    log = "state changed: %s" % state_desc_[state]
    rospy.loginfo(log)
    if state_ == 0:
        resp = srv_client_go_to_point_(True)
        resp = srv_client_wall_follower_(False)
    if state_ == 1:
        resp = srv_client_go_to_point_(False)
        resp = srv_client_wall_follower_(True)
        

def normalize_angle(angle):
    if(math.fabs(angle) > math.pi):
        angle = angle - (2 * math.pi * angle) / (math.fabs(angle))
    return angle

Finally, our main function:

def main():
    global regions_, position_, desired_position_, state_, yaw_, yaw_error_allowed_
    global srv_client_go_to_point_, srv_client_wall_follower_
    
    rospy.init_node('bug0')
    
    sub_laser = rospy.Subscriber('/m2wr/laser/scan', LaserScan, clbk_laser)
    sub_odom = rospy.Subscriber('/odom', Odometry, clbk_odom)
    
    srv_client_go_to_point_ = rospy.ServiceProxy('/go_to_point_switch', SetBool)
    srv_client_wall_follower_ = rospy.ServiceProxy('/wall_follower_switch', SetBool)
    
    # initialize going to the point
    change_state(0)
    
    rate = rospy.Rate(20)
    while not rospy.is_shutdown():
        if regions_ == None:
            continue
        
        if state_ == 0:
            if regions_['front'] > 0.15 and regions_['front'] < 1:
                change_state(1)
        
        elif state_ == 1:
            desired_yaw = math.atan2(desired_position_.y - position_.y, desired_position_.x - position_.x)
            err_yaw = normalize_angle(desired_yaw - yaw_)
            
            if math.fabs(err_yaw) < (math.pi / 6) and \
               regions_['front'] > 1.5:
                change_state(0)
            
            if err_yaw > 0 and \
               math.fabs(err_yaw) > (math.pi / 4) and \
               math.fabs(err_yaw) < (math.pi / 2) and \
               regions_['left'] > 1.5:
                change_state(0)
                
            if err_yaw < 0 and \
               math.fabs(err_yaw) > (math.pi / 4) and \
               math.fabs(err_yaw) < (math.pi / 2) and \
               regions_['right'] > 1.5:
                change_state(0)
            
        rate.sleep()

if __name__ == "__main__":
    main()

We are susbcribing the odometry and laser because:

  1. In order to define the state, we need to know where the robot is
  2. The laser tells us if it’s necessary to follow a wall, or if we can go ahead

In both states (0 and 1), we are checking the laser readings. Depending on it, we can change the state to the other one.

Let’s check if it works!

 


 

Step 5 – Launching Bug 0

Finally, we create a launch file (~/catkin_ws/src/motion_plan/launch/bug0.launch), in order to start everything in a single command.

Here it goes the way we propose:

<launch>
    <arg name="des_x" />
    <arg name="des_y" />
    <param name="des_pos_x" value="$(arg des_x)" />
    <param name="des_pos_y" value="$(arg des_y)" />
    <node pkg="motion_plan" type="follow_wall.py"
          name="wall_follower" output="screen" />
    <node pkg="motion_plan" type="go_to_point.py"
          name="go_to_point" output="screen" />
    <node pkg="motion_plan" type="bug0.py"
          name="bug0" output="screen" />
</launch>

Let’s test it!


Step 6 – Testing

We first launch the world we have been using in the previous post. (my_worlds world.launch)

And from a web shell, spawn the robot at the given position:

roslaunch m2wr_description spawn.launch x:=0 y:=8

Wait a few seconds, when the robot is ready, start the launch file with the Bug 0 algorithm:

roslaunch motion_plan bug0.launch des_x:=0 des_y:=-8

What is happening there?

Below we see an image describing the steps and state changes during the Bug 0 motion:

It starts going to the point, finds a wall, circumnavigates it, go to the point again..

And this happens some times until it reaches the goal!


Step 7 – Conclusion

If you lost some step, you can always clone the ROSject generated from this series. Here it goes the one for the current one: http://www.rosject.io/l/a283a96/

In the next post, we will explore a failure case for Bug 0 and look to another solution: Bug 1 algorithm!

Cheers!

[irp posts=”13273″ name=”Exploring ROS with a 2 wheeled robot #7 – Wall Follower Algorithm”]

Exploring ROS with a 2 wheeled robot #7 – Wall Follower Algorithm

Exploring ROS with a 2 wheeled robot #7 – Wall Follower Algorithm

Wall Follower Algorithm

Hello ROS Developers! In this post number #7, as we continue on the track of the video series, we are going to go line-by-line in order to achieve the Wall Follower Algorithm.

In order to start, you can use the ROSject we generate in the previous post, by copying it here.

Our goal is to achieve the following behavior:

  1. Find the wall – Go straight+right until it reaches an obstacle
  2. Turn left, if there is no obstacle in front of the robot
  3. Go straight ahead, if there is an obstacle in the right side

Which is described in the image below:

Exploring ROS with a 2 wheeled robot #7 - Wall Follower Algorithm

Let’s start!


STEP 1

First things first, creating a new script file for the algorithm. I’m calling it follow_wall.py, and  it goes into the folder ~/catkin_ws/src/motion_plan/scripts/.

Some necessary libraries for our script:

#! /usr/bin/env python

# import ros stuff
import rospy
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
from nav_msgs.msg import Odometry
from tf import transformations

import math

And just below, our global variables:

pub_ = None
regions_ = {
    'right': 0,
    'fright': 0,
    'front': 0,
    'fleft': 0,
    'left': 0,
}
state_ = 0
state_dict_ = {
    0: 'find the wall',
    1: 'turn left',
    2: 'follow the wall',
}

We are again separating the laser regions (right, front-right, front, front-left and left)
And our state_dict, which defines 3 main states: find the wallturn left and follow the wall

STEP 2

Further, two necessary functions, but no magic or business logic here:

def clbk_laser(msg):
    global regions_
    regions_ = {
        'right':  min(min(msg.ranges[0:143]), 10),
        'fright': min(min(msg.ranges[144:287]), 10),
        'front':  min(min(msg.ranges[288:431]), 10),
        'fleft':  min(min(msg.ranges[432:575]), 10),
        'left':   min(min(msg.ranges[576:713]), 10),
    }

    take_action()


def change_state(state):
    global state_, state_dict_
    if state is not state_:
        print 'Wall follower - [%s] - %s' % (state, state_dict_[state])
        state_ = state

In the callback, we assign the means of the laser reading to the regions we defined
And the change_state is just a formal way to change the state, log and change the value of the variable state_

STEP 3

And now, we define the main function, the one that contains the logic of the wall following algorithm. It’s called here take_action:

def take_action():
    global regions_
    regions = regions_
    msg = Twist()
    linear_x = 0
    angular_z = 0
    
    state_description = ''
    
    d = 1.5
    
    if regions['front'] > d and regions['fleft'] > d and regions['fright'] > d:
        state_description = 'case 1 - nothing'
        change_state(0)
    elif regions['front'] < d and regions['fleft'] > d and regions['fright'] > d:
        state_description = 'case 2 - front'
        change_state(1)
    elif regions['front'] > d and regions['fleft'] > d and regions['fright'] < d:
        state_description = 'case 3 - fright'
        change_state(2)
    elif regions['front'] > d and regions['fleft'] < d and regions['fright'] > d:
        state_description = 'case 4 - fleft'
        change_state(0)
    elif regions['front'] < d and regions['fleft'] > d and regions['fright'] < d:
        state_description = 'case 5 - front and fright'
        change_state(1)
    elif regions['front'] < d and regions['fleft'] < d and regions['fright'] > d:
        state_description = 'case 6 - front and fleft'
        change_state(1)
    elif regions['front'] < d and regions['fleft'] < d and regions['fright'] < d:
        state_description = 'case 7 - front and fleft and fright'
        change_state(1)
    elif regions['front'] > d and regions['fleft'] < d and regions['fright'] < d:
        state_description = 'case 8 - fleft and fright'
        change_state(0)
    else:
        state_description = 'unknown case'
        rospy.loginfo(regions)

We are treating 8 possibilities of the three region readings of the laser (Like in the previous post, but now for a different behavior)

Switch to state Find the wall when:

  1. No obstacles detected
  2. Obstacle on the left side
  3. Obstacles on the left and right sides

For the state Turn left:

  1. Obstacles in front of the robot
  2. Obstacles in front and left of the robot
  3. Obstacles in front and right of the robot
  4. Obstacles in front, left and right of the robot

And finally, we change to the state Follow the wall when:

  1. Obstacle detected only on the right side of the robot

STEP 4

The next thing is to define the behavior of each state:

def find_wall():
    msg = Twist()
    msg.linear.x = 0.2
    msg.angular.z = -0.3
    return msg

def turn_left():
    msg = Twist()
    msg.angular.z = 0.3
    return msg

def follow_the_wall():
    global regions_
    
    msg = Twist()
    msg.linear.x = 0.5
    return msg

This is quite basic, but pay attention to the conventions!! Here we defined to keep the wall on the right side, so if you want to change this convention, you need to play with the signal of the variables. It’s also important it can change depending on the gazebo version and gazebo plugins you are using!! (E.g: During the development of this series, the gazebo version was changed from to 7 and the Z-axis of the driver was changed!)

STEP 5

Finally, we just define our main function, where the main node, publishers, subscribers, and a loop, are defined:

def main():
    global pub_
    
    rospy.init_node('reading_laser')
    
    pub_ = rospy.Publisher('/cmd_vel', Twist, queue_size=1)
    
    sub = rospy.Subscriber('/m2wr/laser/scan', LaserScan, clbk_laser)
    
    rate = rospy.Rate(20)
    while not rospy.is_shutdown():
        msg = Twist()
        if state_ == 0:
            msg = find_wall()
        elif state_ == 1:
            msg = turn_left()
        elif state_ == 2:
            msg = follow_the_wall()
            pass
        else:
            rospy.logerr('Unknown state!')
        
        pub_.publish(msg)
        
        rate.sleep()

if __name__ == '__main__':
    main()

After having it running, you may have the robot going around one of the walls. Don’t forget to spawn the robot near to a wall, or just spawn it anywhere and change its position manually, using Gazebo interface.

You must have something like the below:


I hope you liked this post!

If something went wrong in the process of following this tutorial, you can clone the ROSject generated here.

In the next post, we are gonna work with the Bug 0 algorithm using the previous scripts we have created: Wall following + Go to point!

Keep following our channels!

 

Cheers

[irp posts=”13235″ name=”Exploring ROS with a 2 wheeled Robot #6 – Robot Motion Planning”]

Exploring ROS with a 2 wheeled Robot #6 – Robot Motion Planning

Exploring ROS with a 2 wheeled Robot #6 – Robot Motion Planning

Robotic Motion Planning

Hello ROS Developers! In this post we continue with the series Exploring ROS with a 2 wheeled Robot. In the previous post, we created an obstacle avoidance algorithm. Today we’ll start working with motion planning! We’re going create an algorithm to go from a point to another using the odometry data to localize the robot.

If you don’t have the ROSject generated from the previous one, just copy it from here http://www.rosject.io/l/9945166/

Let’s model our problem!


The behavior we want to program is basically going in a straight line from a point to another. We have 2 steps:

  1. Twist robot until we achieve the best heading to the endpoint
  2. Go in a straight line, until we are close enough to the final point

It’s a state machine, described by the diagram below:

Exploring ROS with a 2 wheeled Robot #6 - Robot Motion Planning

The first status is Twist robot, until we reach a good enough heading value. The next status is Go straight ahead, this one can go back to Twist robot or Done.

Let’s start coding!


STEP 1

A new script is created: ~/catkin_ws/src/motion_plan/scripts/go_to_point.py. First thing is to add the necessary libraries:

#! /usr/bin/env python

# import ros stuff
import rospy
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist, Point
from nav_msgs.msg import Odometry
from tf import transformations

import math

And also define some variables: publisher, robot state, machine state, goal position and some tune parameters:

# robot state variables
position_ = Point()
yaw_ = 0
# machine state
state_ = 0
# goal
desired_position_ = Point()
desired_position_.x = -3
desired_position_.y = 7
desired_position_.z = 0
# parameters
yaw_precision_ = math.pi / 90 # +/- 2 degree allowed
dist_precision_ = 0.3

# publishers
pub = None

I’ll define the main function now, so we can have an overview of the algorithm. Bare in mind callbacks and helper functions will be written further:

def main():
    global pub
    
    rospy.init_node('go_to_point')
    
    pub = rospy.Publisher('/cmd_vel', Twist, queue_size=1)
    
    sub_odom = rospy.Subscriber('/odom', Odometry, clbk_odom)
    
    rate = rospy.Rate(20)
    while not rospy.is_shutdown():
        if state_ == 0:
            fix_yaw(desired_position_)
        elif state_ == 1:
            go_straight_ahead(desired_position_)
        elif state_ == 2:
            done()
            pass
        else:
            rospy.logerr('Unknown state!')
            pass
        rate.sleep()

if __name__ == '__main__':
    main()

We are basically in a 20hz loop, checking the status in order to send a command to the robot. The status is defined by an integer:

0 – Twist robot (fix_yaw) – Fix robot orientation
1 – Go straight ahead (go_straight_ahead)  – Just go ahead
2 – Done (done) – Stop

Let’s program the behaviors!


STEP 2

In order to get the desired Yaw, let’s program the first method fix_yaw

def fix_yaw(des_pos):
    global yaw_, pub, yaw_precision_, state_
    desired_yaw = math.atan2(des_pos.y - position_.y, des_pos.x - position_.x)
    err_yaw = desired_yaw - yaw_
    
    twist_msg = Twist()
    if math.fabs(err_yaw) > yaw_precision_:
        twist_msg.angular.z = -0.3 if err_yaw > 0 else 0.3
    
    pub.publish(twist_msg)
    
    # state change conditions
    if math.fabs(err_yaw) <= yaw_precision_:
        print 'Yaw error: [%s]' % err_yaw
        change_state(1)

We are using some global variables. Why? Because these values come from a callback, which will be written further (there is nothing new to us in a subscriber, it’s just something that we need, but not related to the motion planning algorithm, just ROS stuff)

There is a calculation of the yaw error, based on this value, we normalize this value in order to turn the robot always to the closest way (left or right).

Finally, publish the twist message and check if the yaw value is good enough (defined in yaw_precision_how much is good enough. You can see it as a tuning parameter)


STEP 3

What about going straight ahead? Seems to be easier, let’s check how we did it!

def go_straight_ahead(des_pos):
    global yaw_, pub, yaw_precision_, state_
    desired_yaw = math.atan2(des_pos.y - position_.y, des_pos.x - position_.x)
    err_yaw = desired_yaw - yaw_
    err_pos = math.sqrt(pow(des_pos.y - position_.y, 2) + pow(des_pos.x - position_.x, 2))
    
    if err_pos > dist_precision_:
        twist_msg = Twist()
        twist_msg.linear.x = 0.3
        pub.publish(twist_msg)
    else:
        print 'Position error: [%s]' % err_pos
        change_state(2)
    
    # state change conditions
    if math.fabs(err_yaw) > yaw_precision_:
        print 'Yaw error: [%s]' % err_yaw
        change_state(0)

There are 2 verifications here:

  1. Is yaw good enough?
    1. No – Back to state 0
    2. Yes – Keep going
  2. Is the robot close enough to the goal?
    1. No – Keep going
    2. Yes – Go to state 2 (stop)

Again, we are using the global vars in order to get yaw, state, yaw_precision_ and pub


STEP 4

Finally, the done function and other helper functions:

def done():
    twist_msg = Twist()
    twist_msg.linear.x = 0
    twist_msg.angular.z = 0
    pub.publish(twist_msg)

The change_state function, that provides us a log message and changes the global state var:

def change_state(state):
    global state_
    state_ = state
    print 'State changed to [%s]' % state_

And something we don’t program without! The callback for the odometry reading:

def clbk_odom(msg):
    global position_
    global yaw_
    
    # position
    position_ = msg.pose.pose.position
    
    # yaw
    quaternion = (
        msg.pose.pose.orientation.x,
        msg.pose.pose.orientation.y,
        msg.pose.pose.orientation.z,
        msg.pose.pose.orientation.w)
    euler = transformations.euler_from_quaternion(quaternion)
    yaw_ = euler[2]

Which is basically reading the messages and filling our vars. Notice how we are converting the quaternion to simple yaw value. That’s why we need to import tf library.

We are good to go!


STEP 5

Launch the same world we launched before!

Spawn the robot in a suitable position for this test:

roslaunch m2wr_description spawn.launch y:=8

Launch the script:

rosrun motion_plan go_to_point.py

You may notice the robot switches between go_straight_ahead and fix_yaw many times:

Yaw error: [-0.0682238880539]
State changed to [1]
Yaw error: [0.0698676056395]
State changed to [0]
Yaw error: [0.0683643359256]
State changed to [1]
Yaw error: [0.0708310727095]
State changed to [0]
Yaw error: [0.0579104782174]
State changed to [1]
Yaw error: [0.0718681960156]
State changed to [0]
Yaw error: [0.0595617709548]
State changed to [1]
Yaw error: [0.0717480008102]
State changed to [0]
Yaw error: [0.0627887992796]
State changed to [1]
Yaw error: [0.0718602150916]
State changed to [0]
Yaw error: [0.0662619015533]
State changed to [1]
Position error: [0.296183858313]
State changed to [2]

This’s because our robot model is not symmetric, so it lost the heading when it tries to go in a straight line. It can happen with real robots, not only because of the model, but also because of the environment. Something that must be expected!


That’s all for this post!

Remember to leave a comment, if you have doubts or suggestions!

If something went wrong up to here, you can always clone the ROSject produced with this post!

In this next tutorial, we’re going work with wall following robot algorithm. Cheers!

[irp posts=”13195″ name=”Exploring ROS using a 2 Wheeled Robot: Obstacle Avoidance”]

Exploring ROS with a 2 Wheeled Robot #5 – Obstacle Avoidance

Exploring ROS with a 2 Wheeled Robot #5 – Obstacle Avoidance

Obstacle Avoidance

In this tutorial, we will learn what is an obstacle avoidance algorithm and how it works. Let’s go!

STEP 1

First thing, let’s open our ROSject finished in the previous post. If you don’t have it, just copy my own version from here: http://www.rosject.io/l/96491bc/


I start launching the new world (with obstacles) we created for this chapter. The file content is right here, you must copy and paste to the file ~/simulation_ws/src/my_worlds/worlds/world2.launch

And modify the launch file (~/simulation_ws/src/my_worlds/launch_world.launch)) in order to be like below:

<?xml version="1.0" encoding="UTF-8"?>

<launch>
  <arg name="robot" default="machines"/>
  <arg name="debug" default="false"/>
  <arg name="gui" default="true"/>
  <arg name="headless" default="false"/>
  <arg name="pause" default="false"/>
  <arg name="world" default="world02" />
  <include file="$(find gazebo_ros)/launch/empty_world.launch">
    <arg name="world_name" value="$(find my_worlds)/worlds/world01.world"/>
    <arg name="debug" value="$(arg debug)" />
    <arg name="gui" value="$(arg gui)" />
    <arg name="paused" value="$(arg pause)"/>
    <arg name="use_sim_time" value="true"/>
    <arg name="headless" value="$(arg headless)"/>
    <env name="GAZEBO_MODEL_PATH" value="$(find simulation_gazebo)/models:$(optenv GAZEBO_MODEL_PATH)"/>
  </include>
</launch>

Launch the world from ROSDS simulation launcher, as we did in the previous chapter/post. You must have the simulation below running:

STEP 2

Spawn the robot from a web shell, but this time passing some arguments, in order to avoid the wall (otherwise the robot will be spawned just over one of the obstacles right way):

roslaunch m2wr_description spawn.launch y:=8

This is the environment we start it from!


Since we will code with Python, let’s create one more script into the motion_plan package. It goes at ~/catkin_ws/src/motion_plan/scripts/obstacle_avoidance.py. !! Don’t forget to assign the necessary permission to it [chmod +x <name_of_the_file.py>] !!

Let’s go step-by-step!

We defined, in the previous chapter, 5 regions of the laser scan and we are going to use the same here. They are:

  • Right
  • Front-Right
  • Front
  • Front-Left
  • Left

For the sake of simplicity, we only use the 3 ones in the middle to do the obstacle avoidance algorithm.

STEP 3

In our code, let’s start importing the necessary libraries:

#! /usr/bin/env python

import rospy

from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist

Then, we define a global for the publisher in charge of setting the robot speed:

pub = None

At the end of the file, we define the main function and call it to initialize everything:

def main():
    global pub

    rospy.init_node('reading_laser')

    pub = rospy.Publisher('/cmd_vel', Twist, queue_size=1)

    sub = rospy.Subscriber('/m2wr/laser/scan', LaserScan, clbk_laser)

    rospy.spin()

if __name__ == '__main__':
    main()

STEP 4

Now, between the libraries and the main method, we define the method that receives the laser readings through the subscriber. The 5 regions are defined in a dictionary of Python.

def clbk_laser(msg):
    regions = {
        'right':  min(min(msg.ranges[0:143]), 10),
        'fright': min(min(msg.ranges[144:287]), 10),
        'front':  min(min(msg.ranges[288:431]), 10),
        'fleft':  min(min(msg.ranges[432:575]), 10),
        'left':   min(min(msg.ranges[576:713]), 10),
    }

    take_action(regions)

At the end of the laser reading, we call the method take_action. Let’s define it:

def take_action(regions):
    msg = Twist()
    linear_x = 0
    angular_z = 0

    state_description = ''

    if regions['front'] > 1 and regions['fleft'] > 1 and regions['fright'] > 1:
        state_description = 'case 1 - nothing'
        linear_x = 0.6
        angular_z = 0
    elif regions['front'] < 1 and regions['fleft'] > 1 and regions['fright'] > 1:
        state_description = 'case 2 - front'
        linear_x = 0
        angular_z = -0.3
    elif regions['front'] > 1 and regions['fleft'] > 1 and regions['fright'] < 1:
        state_description = 'case 3 - fright'
        linear_x = 0
        angular_z = -0.3
    elif regions['front'] > 1 and regions['fleft'] < 1 and regions['fright'] > 1:
        state_description = 'case 4 - fleft'
        linear_x = 0
        angular_z = 0.3
    elif regions['front'] < 1 and regions['fleft'] > 1 and regions['fright'] < 1:
        state_description = 'case 5 - front and fright'
        linear_x = 0
        angular_z = -0.3
    elif regions['front'] < 1 and regions['fleft'] < 1 and regions['fright'] > 1:
        state_description = 'case 6 - front and fleft'
        linear_x = 0
        angular_z = 0.3
    elif regions['front'] < 1 and regions['fleft'] < 1 and regions['fright'] < 1:
        state_description = 'case 7 - front and fleft and fright'
        linear_x = 0
        angular_z = -0.3
    elif regions['front'] > 1 and regions['fleft'] < 1 and regions['fright'] < 1:
        state_description = 'case 8 - fleft and fright'
        linear_x = 0
        angular_z = -0.3
    else:
        state_description = 'unknown case'
        rospy.loginfo(regions)

    rospy.loginfo(state_description)
    msg.linear.x = linear_x
    msg.angular.z = angular_z
    pub.publish(msg)

This one is a bit long, but the logic of it is not that complex. Let’s group the if-cases and analyze their behavior:

If there is nothing in any of the 3 areas of the laser, the robot just goes ahead, in a straight line.

state_description = 'case 1 - nothing'
linear_x = 0.6
angular_z = 0

For the case “2”, “7” and “8”, there is something in front or all the regions. We just defined to turn left on these cases (positive angular value).

So, for the cases “3” and “5”, when there is something on the right side, we turn to the left again.

Finally, cases “4” and “6”, just turn to the right (negative angular value)

At the end of the function, we just log some data for debugging purposes, set the twist message and publish it.


Run the script and see the robot avoiding the obstacles!

 

I hope everything went well in your ROSject. If not, remember you can always leave a comment or contact us using any of our channels. If you need, you can also copy the final ROSject we generated from this post!

In this next post we are going create an algorithm to go from a point to another using the odometry data to localize the robot!

Cheers!

Exploring ROS with a 2 Wheeled Robot #4 – Laser Scan Reading

Exploring ROS with a 2 Wheeled Robot #4 – Laser Scan Reading

Hello ROS Developers!

In this post, we summarize the video – Exploring ROS using a 2 Wheeled Robot ep. 4, where we start using the Laser Scan data. Let’s start!

First thing, let’s open our ROSject finished with the previous post.

We had some minor adjustments (in order to calibrate the robot and fix some minor issues), that you can track here. They are the following:

  • Adjust robot size and calibrate differential drive
  • Update CMakeLists.txt
  • Create a world file, that we are going to use in the next episodes
  • Create a launch file to start the world

The ROSject we finished in the last post is this one, but, in order to get the one updated (with the fixed I mentioned), click here.

You must be able to launch a simulation like below:

 

After that, let’s start with the laser part!

Let’s start creating a new package, but this time at catkin_ws. This is because we are not creating a simulation, instead it’s a motion planning package, that will be able to command any kind of mobile robot! It’s important to understand we are not developing a package strict to our robot, but something generic.

In a web shell, execute the following:

user:~$ cd ~/catkin_ws/src/
user:~$ catkin_create_pkg motion_plan std_msgs geometry_msgs rospy sensor_msgs

And create our first script that interacts with the laser data> ~/catkin_ws/src/motion_plan/scripts/reading_laser.py:

#! /usr/bin/env python

import rospy

from sensor_msgs.msg import LaserScan

def clbk_laser(msg):
    # 720 / 5 = 144
    regions = [
        min(min(msg.ranges[0:143]), 10),
        min(min(msg.ranges[144:287]), 10),
        min(min(msg.ranges[288:431]), 10),
        min(min(msg.ranges[432:575]), 10),
        min(min(msg.ranges[576:713]), 10),
    ]
    rospy.loginfo(regions)

def main():
    rospy.init_node('reading_laser')

    sub = rospy.Subscriber('/m2wr/laser/scan', LaserScan, clbk_laser)

    rospy.spin()

if __name__ == '__main__':
    main()

Using a web shell, spawn the robot in the simulation we have running:

user:~$ roslaunch m2wr_description spawn.launch

With the spawning process finished, start the new script we`ve just created:

user:~$ rosrun motion_plan reading_laser.py

You shall see an output like the below:

[INFO] [1556996996.074220, 1870.728000]: [10, 8.899812698364258, 1.7639966011047363, 0.9439168572425842, 0.8716692924499512]
[INFO] [1556996996.127200, 1870.781000]: [10, 8.92310905456543, 1.7670421600341797, 0.9425056576728821, 0.8715123534202576]
[INFO] [1556996996.176019, 1870.830000]: [10, 8.915216445922852, 1.770876407623291, 0.9503755569458008, 0.8712261319160461]
[INFO] [1556996996.226823, 1870.881000]: [10, 8.923232078552246, 1.7670140266418457, 0.942345380783081, 0.8683222532272339]
[INFO] [1556996996.277591, 1870.931000]: [10, 8.91376781463623, 1.7766363620758057, 0.945663332939148, 0.8739328980445862]
[INFO] [1556996996.327826, 1870.981000]: [10, 8.925933837890625, 1.7535350322723389, 0.954210638999939, 0.8760458827018738]
[INFO] [1556996996.378767, 1871.031000]: [10, 8.916155815124512, 1.7713392972946167, 0.9581797122955322, 0.866176962852478]
[INFO] [1556996996.428421, 1871.081000]: [10, 8.912318229675293, 1.7645303010940552, 0.9598914384841919, 0.8663960695266724]
[INFO] [1556996996.478520, 1871.130000]: [10, 8.918071746826172, 1.786738634109497, 0.9538688659667969, 0.8684337139129639]
[INFO] [1556996996.529212, 1871.180000]: [10, 8.918127059936523, 1.7791880369186401, 0.9522199034690857, 0.8771331906318665]

This is a print of an array which contains 5 means of the laser, summarized in the image below:

You can move the robot using `cmd_vel` or using gazebo drag tool in order to see the values changing. In the image below we have the bottom of a shell under the gazebo window. You can check the 5th position of the array represents the area on the left side of the robot. In the other side, we have maximum value being read: 10 meters, on the right side of the robot.

In case something went wrong up to here, you can get a copy of the final version of this post here.

Feel free to explore the code, navigate using teleop or a custom node. In the next post, we’ll work on an algorithm to make the robot avoid obstacles in the world!

 

Cheers!

Pin It on Pinterest