[Semester Project] Multi Robot Challenge

For the group project you will work as part of a team to solve a Search and Rescue (S&R) scenario with a small team of mobile robots. Typical skills to be acquired/demonstrated in the project include mobile robot control, navigation and localisation, robot teams, robot software architectures, and the Robot Operating System (ROS). A more detailed description can be found in canvas in Filer/Semester Projects/DAT160_semester_prosjekt_H22.pdf

Setup Process

  • Download the ROS package here

  • Copy it into catkin_ws/src folder

  • Delete the build folder in your catkin_ws

  • In a terminal run:

cd ~/catkin_ws/
catkin_make
source devel/setup.bash
rospack profile
  • Copy the folders inside the models folder of the ROS package and copy them into ~/.gazebo/models/

  • To spawn the robots in the different worlds use the launch files rescue_robots_w1.launch - w5.launch.

Namespaces

In ROS, Namespaces are used to allow multiple topics with the same name to exists. It works similar to a folder structure in any operating system, where inside one folder no duplicate names are allowed but in a different folder a file can have the exact same name. We therefore use Namespaces to have multiple topics with the same name e. g. when launching multiple Turtlebots we need two different “cmd_vel” topics to control each of the robots. The solution to the two exercises can be downloaded here.

Inside a launch file you can use the following code to launch a node inside a namepsace. Don’t forget to replace the in uppercase written names.

<group ns="NAMESPACE_NAME">
    <node pkg="ROS_PACKAGE_NAME" name="NODE_NAME" type="NAME_OF_PYTHON_SCRIPT" output="screen" />
</group>

When referencing topic names inside a Namepspace be aware of the syntax. Defining the name with a leading “/” means that I will defined the full name with all Namespaces e.g. “/tb3_0/odom”. If you don’t put the leading “/” you are defining the name from the Namespace you are in. If I launch my ROS Node inside the Namespace “tb3_0” I can then reference the same topic as before with just “odom”.

Exercise 1 - Namespaces

Inside the given ROS package multi_robot_challenge_22 do the following steps:

  • Create 2 launch files which start a teleoperation keyboard for each robot using the following example code:

<launch>
  <group ns = "NAMESPACE_NAME">
    <node pkg="turtlebot3_teleop" type="turtlebot3_teleop_key" name="turtlebot3_teleop_keyboard"  output="screen" />
  </group>
</launch>
  • Create a python class used for the robot controller

  • Create a python class used for the leader

  • Create a launch file which launches the robot controller class 2 times in the same Namespaces as the Turtlebots and the leader class outside of any Namespaces

  • Each robot controller class should subscribe to the LiDAR topic of the Turtlebot in the same Namespace and publish a single value in a topic called “namespace_com”

  • The leader class should subscribe to both “namespace_com” Topics and print out whatever is received. Add an identifier to the print out so that you know which value comes from which robot class.

Exercise 2 - AR-Tags

Continue inside the ROS package multi_robot_challenge_22 with the following steps:

  • In the robot class, create a subscriber to the topic “ar_pose_marker”.

  • If any AR Tags are registered publish the id in a topic “marker_id”

  • In the leader class, subscribe to both “marker_id” topics and print out what has been received.

  • To check your work use the teleop keyboard to move the Turtlebots in front of an AR Tag

Occupancy Grid

In ROS, Occupancy Grid is a data type which represents a 2 dimensional grid map in which each cell gives the probability of an obstacle being at that position. Normally this type of map is produced with a robot moving through an environment and mapping it using a SLAM algorithm. The Occupancy grid data is given in a 1 dimensional array in row-major order starting at the bottom right corner of the map. The data values have the following meaning:

  • -1 = unkown

  • 0-100 = probability of occupancy. Which means 0 is definitely no obstacle and 100 is definitely an obstacle.

The Occupancy Grids also carry other useful meta data:

../../_images/occupancy_grid.PNG

The following code was produced during the lecture and is an example of how to work with Occupancy Grids:

Example Python Code for handeling OccupancyGrid data
#! /usr/bin/env python

import rospy
from nav_msgs.msg import OccupancyGrid
from visualization_msgs.msg import Marker
from geometry_msgs.msg import Point


class MapInterpreterClass:
    def __init__(self):
        rospy.init_node("MapInterpreterClass", anonymous =False)
        self.map_sub = rospy.Subscriber("/map", OccupancyGrid, self.clbk_map)

        self.map = OccupancyGrid()
        while len(self.map.data) < 1:
            rospy.loginfo("waiting for map...")
            rospy.Rate(1).sleep()

        self.borders_pub = rospy.Publisher('/borders', Marker, queue_size=10)
        self.border_points = Marker()
        self.border_points.header.frame_id = self.map.header.frame_id
        self.border_points.ns = "markers"
        self.border_points.id = 0
        self.border_points.type = self.border_points.POINTS
        self.border_points.action = self.border_points.ADD
        self.border_points.pose.orientation.w =1.0
        self.border_points.scale.x=0.1
        self.border_points.scale.y=0.1
        self.border_points.color.r = 255.0/255.0
        self.border_points.color.g = 0.0/255.0
        self.border_points.color.b = 0.0/255.0
        self.border_points.color.a = 1
        self.border_points.lifetime = rospy.Duration()

    def clbk_map(self, msg):
        self.map = msg

    def get_map_pos(self, map_iter):
        map_pos = Point()
        map_pos.x = int(map_iter/self.map.info.width)
        map_pos.y = map_iter - map_pos.x*self.map.info.width

        return map_pos

    def getPosition(self, x, y):
        map_position = Point()
        map_position.x = self.map.info.origin.position.x + y*self.map.info.resolution
        map_position.y = self.map.info.origin.position.y + x*self.map.info.resolution

        return map_position

    def run(self):
        r = rospy.Rate(1)



        for i in range(len(self.map.data)):
            if self.map.data[i] > 80:
                map_iter = i
                break

        map_pos = self.get_map_pos(map_iter)
        rospy.loginfo(map_pos)
        map_position = self.getPosition(map_pos.x, map_pos.y)
        rospy.loginfo(map_position)



        while True:

            self.border_points.points = [map_position]
            self.borders_pub.publish(self.border_points)
            r.sleep()



if __name__ == '__main__':
    mi = MapInterpreterClass()
    mi.run()