iRobot Create 2 - Simulation

In my previous post I mentioned that I was starting to experiment with an iRobot Create 2. With the development environment setup the next step is setting up Gazebo simulations.

There wasn’t much available upstream for setting up simulation, so I did most of it myself poking around Github for any examples I could find. All of the robot’s sensors needed to be modeled and publishing appropriate create_msgs messages.

All of the following will be in a catkin package called create_gazebo.

Spawning

First setup launch files to spawn the create robot in an empty world. This will using Gazebo’s pre-installed empty.world file.

<!-- gazebo.launch -->
<?xml version="1.0"?>
<launch>
  <arg name="use_sim_time" default="true" />
  <arg name="gui" default="true" />
  <arg name="headless" default="false" />
  <arg name="world" default="worlds/empty.world" />
  <arg name="verbose" default="false" />

  <include file="$(find gazebo_ros)/launch/empty_world.launch">
    <arg name="gui" value="$(arg gui)" />
    <arg name="use_sim_time" value="$(arg use_sim_time)" />
    <arg name="headless" value="$(arg headless)" />
    <arg name="world_name" value="$(arg world)" />
    <arg name="debug" value="0" />
    <arg name="verbose" value="$(arg verbose)" />
  </include>

  <include file="$(find create_gazebo)/launch/spawn_create_2.launch"/>
</launch>

At the bottom, another launch file is included to spawn the robot.

<!-- spawn_create_2.launch -->
<?xml version="1.0"?>
<launch>
  <!-- Setup robot description and spawn into Gazebo sim -->
  <include file="$(find create_description)/launch/create_2.launch" />
  <node name="spawn_create2" pkg="gazebo_ros" type="spawn_model" args="-param robot_description -urdf -model robot" />
</launch>

The create_description package is provided by upstream with the necessary URDF files. We’ll come back to those files later when adding the sensors. My fork of the upstream packages is here.

That is all that is needed to spawn the robot in Gazebo, and the robot can be commanded velocities as is (since the diff drive plugins are loaded by the URDF file already). Navigation and move_base setup will come in a following post.

Sensors

Now that the robot in spawned in a simulated world, it needs to interact with its environment.

The iRobot Create 2 has the following sensors along the bumper that can be used to sense the environment:

The general approach I took to sensor plugins was that there would be one sensor plugin allocated per sensor, which publishes some representation of the sensor data, and a model plugin that consumes the simulated sensor data and publishes the create_msgs messages.

simulated world -> sensor plugins -> simulated sensor data -> model plugin -> create_msgs

Building and Installing Plugins

Package XML dependencies and export tags:

  ...
  <depend>create_msgs</depend>
  <depend>create_navigation</depend>
  <depend>gazebo_msgs</depend>
  <depend>gazebo_ros</depend>
  <depend>gazebo_plugins</depend>
  <depend>nav_msgs</depend>
  <depend>roscpp</depend>
  <depend>std_msgs</depend>
  <depend>tf</depend>

  <exec_depend>create_description</exec_depend>

  <export>
    <gazebo_ros plugin_path="${prefix}/lib"/>
  </export>
  ...
cmake_minimum_required(VERSION 3.0.2)
project(create_gazebo)

add_compile_options(-std=c++17 -Wall -Wextra)

find_package(catkin REQUIRED COMPONENTS
    create_msgs
    gazebo_msgs
    gazebo_plugins
    gazebo_ros
    nav_msgs
    roscpp
    std_msgs
    tf
)

find_package(gazebo REQUIRED)

catkin_package(
    CATKIN_DEPENDS
        create_msgs
        gazebo_msgs
        gazebo_plugins
        gazebo_ros
        nav_msgs
        roscpp
        std_msgs
        tf
    LIBRARIES
        create_bumper_model_plugin
        create_cliff_sensor_plugin
)

#-----------------------------------------------------------------------------------------------------------------------
# Gazebo plugins
#-----------------------------------------------------------------------------------------------------------------------
link_directories(${GAZEBO_LIBRARY_DIRS})

# Bumper plugin
add_library(create_bumper_model_plugin
    src/plugins/bumper_model_plugin.cpp
)
target_include_directories(create_bumper_model_plugin
    PUBLIC
        include/
        ${catkin_INCLUDE_DIRS}
        ${GAZEBO_INCLUDE_DIRS}
)
target_link_libraries(create_bumper_model_plugin
    ${catkin_LIBRARIES} ${GAZEBO_LIBRARIES} RayPlugin
)
add_dependencies(create_bumper_model_plugin ${catkin_EXPORTED_TARGETS})

# Cliff sensor plugin
add_library(create_cliff_sensor_plugin
    src/plugins/cliff_sensor_plugin.cpp
)
target_include_directories(create_cliff_sensor_plugin
    PUBLIC
        include/
        ${catkin_INCLUDE_DIRS}
        ${GAZEBO_INCLUDE_DIRS}
)
target_link_libraries(create_cliff_sensor_plugin
    ${catkin_LIBRARIES} ${GAZEBO_LIBRARIES} RayPlugin
)
add_dependencies(create_cliff_sensor_plugin ${catkin_EXPORTED_TARGETS})

# Cliff Model Plugin
add_library(create_cliff_model_plugin
    src/plugins/cliff_model_plugin.cpp
)
target_include_directories(create_cliff_model_plugin
    PUBLIC
        include/
        ${catkin_INCLUDE_DIRS}
        ${GAZEBO_INCLUDE_DIRS}
)
target_link_libraries(create_cliff_model_plugin
    ${catkin_LIBRARIES} ${GAZEBO_LIBRARIES}
)
add_dependencies(create_cliff_model_plugin ${catkin_EXPORTED_TARGETS})

# Light sensor plugin
add_library(create_light_sensor_plugin
    src/plugins/light_sensor_plugin.cpp
)
target_include_directories(create_light_sensor_plugin
    PUBLIC
        include/
        ${catkin_INCLUDE_DIRS}
        ${GAZEBO_INCLUDE_DIRS}
)
target_link_libraries(create_light_sensor_plugin
    ${catkin_LIBRARIES} ${GAZEBO_LIBRARIES} RayPlugin
)
add_dependencies(create_light_sensor_plugin ${catkin_EXPORTED_TARGETS})

#-----------------------------------------------------------------------------------------------------------------------
# Installs
#-----------------------------------------------------------------------------------------------------------------------
install(
    TARGETS
        create_bumper_model_plugin
        create_cliff_sensor_plugin
        create_cliff_model_plugin
        create_light_sensor_plugin
    DESTINATION         ${CATKIN_PACKAGE_BIN_DESTINATION}
    LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
)

install(
    DIRECTORY launch
    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)

Bump / Contact Sensors

Gazebo already has a plugin for bump detection, which derives from a contact sensor. It publishes a gazebo_msgs/ContactsState message.

The URDF file gives the base_link a collision cylinder. I attached the bump detector to the base_link collision object since that will allow detecting collisions anywhere on the robot’s body.

For reference here is what the base_link looks like:

<link name="base_link">
  <inertial>
    <mass value="2" />
    <origin xyz="0 0 0.0" />
    <inertia ixx="0.01" ixy="0.0" ixz="0.0" iyy="0.01" iyz="0.0" izz="0.5" />
  </inertial>

  <visual>
    <origin xyz=" 0 0 0.0308" rpy="0 0 0" />
    <geometry>
      <xacro:insert_block name="mesh" />
    </geometry>
  </visual>

  <collision>
    <origin xyz="0.0 0.0 0.0308" rpy="0 0 0" />
    <geometry>
      <cylinder length="0.0611632" radius="0.16495" />
    </geometry>
  </collision>
</link>

In create_base_gazebo.urdf.xacro I added a new macro to setup bump detection.

<xacro:macro name="sim_create_bump_sensor">
  <gazebo reference="base_link">
    <sensor name="bumper" type="contact">
      <always_on>true</always_on>
      <update_rate>20.0</update_rate>
      <pose>0 0 0 0 0 0</pose>
      <visualize>false</visualize>
      <contact>
        <collision>base_footprint_fixed_joint_lump__base_link_collision_1</collision>
      </contact>
      <plugin name="gazebo_ros_bumper_controller" filename="libgazebo_ros_bumper.so">
        <bumperTopicName>/sim/bumper</bumperTopicName>
      </plugin>
    </sensor>
  </gazebo>
</xacro:macro>

Regarding the collision object name:

I found out that when the URDF file is converted to a SDF file, a collision name is generated and referenced by the sensor plugin. I wasn’t sure how to get it to use its assigned name explicitly. So the way to get the generated name is to convert the URDF file to SDF and inspect the contents.

xacro ./src/create_robot/create_description/urdf/create_2.urdf.xacro > test.urdf
gz sdf -p test.urdf > test.sdf

Not ideal but that’s what works.

Now the Gazebo contact state must be converted to a create_msgs/Bumper message type. This will be done using a model plugin.

This plugin must subscribe to the contact state message from Gazebo, only consider collisions on the front half of the robot, determine if the bump is on the left or right sensor and finally publish the create_msgs/Bumper message.

namespace gazebo
{
/**
 * Model plugin that consolidates bumper sensor states into a single create_msgs/Bumper message
*/
class CreateBumperModelRos : public ModelPlugin
{
public:
    CreateBumperModelRos();
    ~CreateBumperModelRos() = default;

    void Load(physics::ModelPtr parent, sdf::ElementPtr sdf);

private:
    /**
     * Contact sensor callback
    */
    void sensorCallback(const gazebo_msgs::ContactsStateConstPtr& msg);

    /**
     * Odom callback
    */
    void odomCallback(const nav_msgs::OdometryConstPtr& msg);

    /**
     * Light sensor callback
    */
    void lightSensorCallback(const std_msgs::UInt16ConstPtr& msg, uint16_t& field);

    /**
     * Update bumper publisher
    */
    void bumperPubTimerCallback(const ros::TimerEvent&);

    // Bumper message, publisher and timer
    ros::Publisher bumper_pub_;
    create_msgs::Bumper bumper_msg_;
    ros::Timer bumper_pub_timer_;
    // Subscriber for contact states
    ros::Subscriber contact_sub_;
    // Odom to get robot heading
    ros::Subscriber odom_sub_;

    // Contact parameters
    double front_contact_threshold_;

    // Robot heading
    tf::Vector3 heading_;
};
}
#include "create_gazebo/plugins/bumper_model_plugin.hpp"

#include "tf/tf.h"

namespace gazebo
{
// Register the plugin with Gazebo
GZ_REGISTER_MODEL_PLUGIN(CreateBumperModelRos)

CreateBumperModelRos::CreateBumperModelRos()
    : front_contact_threshold_{10.0} // Threshold for what is considered a bump at the front of the robot
    , heading_{1, 0, 0}              // The robot's current heading
{
}

void CreateBumperModelRos::Load(physics::ModelPtr, sdf::ElementPtr sdf)
{
    // Initialize ROS
    if (!ros::isInitialized())
    {
        int argc = 0;
        char** argv = nullptr;

        ros::init(argc, argv, "create_bumper_model_plugin");
    }

    // Load parameters
    double updateRate = 10.0;
    if (sdf->HasElement("updateRate"))
    {
        updateRate = sdf->Get<double>("updateRate");
    }

    std::string topic_name{"/bumper"};
    if (sdf->HasElement("topicName"))
    {
        topic_name = sdf->Get<std::string>("topicName");
    }

    std::string odom_topic{"/odom"};
    if (sdf->HasElement("odomTopic"))
    {
        odom_topic = sdf->Get<std::string>("odomTopic");
    }

    if (sdf->HasElement("frontContactThreshold"))
    {
        front_contact_threshold_ = sdf->Get<double>("frontContactThreshold");
    }

    // Setup publishers
    ros::NodeHandle nh;

    // The publisher for the bumper state
    bumper_pub_ = nh.advertise<create_msgs::Bumper>(topic_name, 1);
    // Timer that publishes the bumper message at {updateRate}
    bumper_pub_timer_ = nh.createTimer(ros::Duration{1.0 / updateRate}, 
                                       &CreateBumperModelRos::bumperPubTimerCallback, this);

    // Subscribe to the gazebo contact state message
    contact_sub_ = nh.subscribe("sim/bumper", 10, &CreateBumperModelRos::sensorCallback, this);

    // Odometry callback for getting the heading of the robot
    odom_sub_ = nh.subscribe(odom_topic, 10, &CreateBumperModelRos::odomCallback, this);
}

void CreateBumperModelRos::sensorCallback(const gazebo_msgs::ContactsStateConstPtr& msg)
{
    bumper_msg_.is_left_pressed = false;
    bumper_msg_.is_right_pressed = false;

    for (const auto& state : msg->states)
    {
        for (const auto& normal : state.contact_normals)
        {
            tf::Vector3 n;
            tf::vector3MsgToTF(normal, n);

            // Contact points are on a cylindrical collision body.
            // Therefore, the contact normals are all pointed inwards to the robot's center (base_link origin)

            // Invert the normal (point outwards from origin)
            n *= -1.0;

            tf::Vector3 x{1.0, 0.0, 0.0};
            // Check if the contact comes from the front of the robot
            if (heading_.dot(n) >= 0.0)
            {
                if (x.angle(n) <= tfRadians(front_contact_threshold_))
                {
                    bumper_msg_.is_left_pressed = true;
                    bumper_msg_.is_right_pressed = true;
                }
                else if (n.y() > 0.0)
                {
                    // left
                    bumper_msg_.is_left_pressed = true;
                    bumper_msg_.is_right_pressed = false;
                }
                else
                {
                    // right
                    bumper_msg_.is_left_pressed = false;
                    bumper_msg_.is_right_pressed = true;
                }
            }
        }
    }
}

void CreateBumperModelRos::odomCallback(const nav_msgs::OdometryConstPtr& msg)
{
    // Convert the orientation from odom into a tf type
    tf::Quaternion r{};
    tf::quaternionMsgToTF(msg->pose.pose.orientation, r);

    // Calculate the heading by rotating a X direction vector by the given quaternion
    const tf::Vector3 d{1, 0, 0};
    heading_ = tf::quatRotate(r, d);
}

void CreateBumperModelRos::bumperPubTimerCallback(const ros::TimerEvent&)
{
    // Update the bumper message
    bumper_pub_.publish(bumper_msg_);
}

}

Adding the model plugin to the URDF:

<gazebo>
  <plugin name="create_bumper_model_plugin" filename="libcreate_bumper_model_plugin.so">
    <updateRate>10.0</updateRate>
  </plugin>
</gazebo>

Cliff Sensors

Gazebo does not have a built in cliff sensor. So I created one based off a ray sensor and attached it to the existing cliff sensor links in the URDF.

namespace gazebo
{
/**
 * iRobot Create Cliff Sensor Plugin
*/
class CreateCliffRos : public RayPlugin
{
public:
    CreateCliffRos();
    ~CreateCliffRos() = default;

    /**
     * Load plugin
    */
    void Load(sensors::SensorPtr parent, sdf::ElementPtr sdf);

private:
    /**
     * Topic connect callback
    */
    void onConnect();
    /**
     * Topic disconnect callback
    */
    void onDisconnect();
    /**
     * Ray scan callback
    */
    void onScan(const ConstLaserScanStampedPtr& msg);

    // Parent sensor
    sensors::RaySensorPtr parent_;

    // Plugin parameters
    float min_range_;

    // ROS publisher
    PubMultiQueue pmq_;
    PubQueue<std_msgs::Bool>::Ptr pub_queue_;
    ros::Publisher pub_;

    // Gazebo subscriber
    transport::NodePtr node_;
    transport::SubscriberPtr scan_sub_;
};
}
#include "create_gazebo/plugins/cliff_sensor_plugin.hpp"

#include <gazebo_plugins/gazebo_ros_utils.h>


namespace gazebo
{
    GZ_REGISTER_SENSOR_PLUGIN(CreateCliffRos)

    CreateCliffRos::CreateCliffRos()
    {
    }

    void CreateCliffRos::Load(sensors::SensorPtr parent, sdf::ElementPtr sdf)
    {
        // Load the base plugin
        RayPlugin::Load(parent, sdf);

        parent_ = std::dynamic_pointer_cast<sensors::RaySensor>(parent);

        if (!parent_)
        {
            gzthrow("Parent sensor must be a Ray sensor");
        }

        // Load plugin parameters
        if (!sdf->HasElement("cliffRange"))
        {
            ROS_INFO_NAMED("cliff", "Cliff plugin missin parameter 'cliffRange', defaulting to 5cm");
            min_range_ = 0.05;
        }
        else
        {
            min_range_ = sdf->Get<double>("cliffRange");
        }

        std::string topic_name = "/cliff";
        if (sdf->HasElement("topicName"))
        {
            topic_name = sdf->Get<std::string>("topicName");
        }

        // Ensure ROS is up
        if (!ros::isInitialized())
        {
            ROS_FATAL_STREAM_NAMED("cliff", "ROS is not initialized, cannot load plugin");
            return;
        }

        // Get the world name and robot namespace
        // These will be used to subscribe to Gazebo laser scan messages (not ROS).
        const auto world_name = parent_->WorldName();
        const auto robot_namespace = GetRobotNamespace(parent_, sdf, "Cliff");

        // Gazebo node is used to subscribe to gazebo laser scan messages
        node_ = transport::NodePtr{new transport::Node{}};
        node_->Init(world_name);

        pmq_.startServiceThread();

        ros::NodeHandle nh{robot_namespace};

        ros::AdvertiseOptions opts = ros::AdvertiseOptions::create<std_msgs::Bool>(
            topic_name, 1,
            std::bind(&CreateCliffRos::onConnect, this),
            std::bind(&CreateCliffRos::onDisconnect, this),
            ros::VoidPtr{}, nullptr
        );

        pub_ = nh.advertise(opts);
        pub_queue_ = pmq_.addPub<std_msgs::Bool>();

        parent_->SetActive(false);

        ROS_INFO_STREAM_NAMED("cliff", "Starting cliff plugin: " << robot_namespace);
    }

    void CreateCliffRos::onConnect()
    {
        if (!scan_sub_)
        {
            // Only process scans in a subscriber is connected
            ROS_INFO_STREAM_NAMED("cliff", "Subscribing to scan messages");
            scan_sub_ = node_->Subscribe(parent_->Topic(), &CreateCliffRos::onScan, this);
        }
    }

    void CreateCliffRos::onDisconnect()
    {
        ROS_INFO_STREAM_NAMED("cliff", "Disconnecting from scan messages");
        scan_sub_.reset();
    }

    void CreateCliffRos::onScan(const ConstLaserScanStampedPtr& laser)
    {
        // If the detected distance is outside the minimum range, indicate that a cliff is detected
        std_msgs::Bool msg;
        msg.data = laser->scan().ranges(0) > min_range_;

        pub_queue_->push(msg, pub_);
    }
}

Attached to links as the following:

<gazebo reference="left_cliff_sensor_link">
  <sensor type="ray" name="left_cliff_sensor">
    <always_on>true</always_on>
    <update_rate>20.0</update_rate>
    <pose>0 0 0 0 0 0</pose>
    <visualize>false</visualize>
    <ray>
      <scan>
        <horizontal>
          <samples>1</samples>
          <resolution>1</resolution>
          <min_angle>0</min_angle>
          <max_angle>0</max_angle>
        </horizontal>
      </scan>
      <range>
        <min>0.01</min>
        <max>0.04</max>
        <resolution>0.1</resolution>
      </range>
    </ray>
    <plugin name="gazebo_left_cliff_sensor" filename="libcreate_cliff_sensor_plugin.so">
      <topicName>/sim/left_cliff</topicName>
      <cliffRange>0.05</cliffRange>
    </plugin>
  </sensor>
</gazebo>

<!--
  Repeated for each sensor
-->

The upstream driver did not contain a cliff sensor message, so I added the following that created a model plugin to publish it.

# Cliff.msg
Header header

bool is_cliff_left
bool is_cliff_front_left
bool is_cliff_front_right
bool is_cliff_right
namespace gazebo
{
/**
 * Model plugin that consolidates individual cliff sensor sim topics into a single
 * /cliff topic that would be emitted by the ROS driver
*/
class CreateCliffModelRos : public ModelPlugin
{
public:
    CreateCliffModelRos();
    ~CreateCliffModelRos() = default;

    void Load(physics::ModelPtr parent, sdf::ElementPtr sdf);

private:
    /**
     * Cliff sensor callback
    */
    void sensorCallback(const std_msgs::BoolConstPtr& msg, bool& field);

    /**
     * 
    */
    void timerCallback(const ros::TimerEvent&);

    // Main cliff message publisher
    ros::Publisher cliff_pub_;
    // Cliff message
    create_msgs::Cliff cliff_msg_;
    // Publish timer
    ros::Timer pub_timer_;

    // Subscribers for individual sensors
    ros::Subscriber left_sub_;
    ros::Subscriber leftfront_sub_;
    ros::Subscriber right_sub_;
    ros::Subscriber rightfront_sub_;

    // Individual sensor states
    bool left_state_;
    bool leftfront_state_;
    bool right_state_;
    bool rightfront_state_;
};
}
#include "create_gazebo/plugins/cliff_model_plugin.hpp"

#include <functional>

namespace gazebo
{
// Register plugin with Gazebo
GZ_REGISTER_MODEL_PLUGIN(CreateCliffModelRos)

CreateCliffModelRos::CreateCliffModelRos()
{
}

void CreateCliffModelRos::Load(physics::ModelPtr, sdf::ElementPtr sdf)
{
    // Initialized ROS
    if (!ros::isInitialized())
    {
        int argc = 0;
        char** argv = nullptr;

        ros::init(argc, argv, "create_cliff_model_plugin");
    }

    // Load parameters
    double updateRate = 10.0;
    if (sdf->HasElement("updateRate"))
    {
        updateRate = sdf->Get<double>("updateRate");
    }

    std::string topic_name{"/cliff"};
    if (sdf->HasElement("topicName"))
    {
        topic_name = sdf->Get<std::string>("topicName");
    }

    ros::NodeHandle nh;

    // Setup publisher
    cliff_pub_ = nh.advertise<create_msgs::Cliff>(topic_name, 1);
    pub_timer_ = nh.createTimer(ros::Duration{1.0 / updateRate}, &CreateCliffModelRos::timerCallback, this);

    // Setup subscribers
    using namespace boost::placeholders;

    left_sub_ = nh.subscribe<std_msgs::Bool>("/sim/left_cliff", 1,
                             boost::bind(&CreateCliffModelRos::sensorCallback, this, _1, boost::ref(left_state_)));
    right_sub_ = nh.subscribe<std_msgs::Bool>("/sim/right_cliff", 1,
                             boost::bind(&CreateCliffModelRos::sensorCallback, this, _1, boost::ref(right_state_)));
    leftfront_sub_ = nh.subscribe<std_msgs::Bool>("/sim/leftfront_cliff", 1,
                             boost::bind(&CreateCliffModelRos::sensorCallback, this, _1, boost::ref(leftfront_state_)));
    rightfront_sub_ = nh.subscribe<std_msgs::Bool>("/sim/rightfront_cliff", 1,
                             boost::bind(&CreateCliffModelRos::sensorCallback, this, _1, boost::ref(rightfront_state_)));
}

void CreateCliffModelRos::sensorCallback(const std_msgs::BoolConstPtr& msg, bool& field)
{
    field = msg->data;
}

void CreateCliffModelRos::timerCallback(const ros::TimerEvent&)
{
    cliff_msg_.header.stamp = ros::Time::now();
    cliff_msg_.header.seq++;

    cliff_msg_.is_cliff_left = left_state_;
    cliff_msg_.is_cliff_front_left = leftfront_state_;
    cliff_msg_.is_cliff_right = right_state_;
    cliff_msg_.is_cliff_front_right = rightfront_state_;

    cliff_pub_.publish(cliff_msg_);
}

}

Adding model plugin to the URDF:

<gazebo>
  <plugin name="create_cliff_model_plugin" filename="libcreate_cliff_model_plugin.so">
    <updateRate>10.0</updateRate>
  </plugin>
</gazebo>

Light sensors

My light sensor plugins are not really “light sensing”. They work like cliff sensors are and based on the distance to the detected object. I did this as the easy root as actually light detection would be base on the material of the nearby object. Hopefully this provides a reasonable approximation of the real thing.

Since light sensors are only on the Create 2, I’ve added the light sensor links to create_2.urdf.xacro:

<link name="left_light_sensor_link">
<inertial>
  <mass value="0.01" />
  <origin xyz="0 0 0" />
  <inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001" />
</inertial>
</link>

And added the light sensor plugins to the create_2_gazebo.urdf.xacro file using a new macro:

  <xacro:macro name="sim_create_light_sensor" params="prefix topic">
    <gazebo reference="${prefix}_link">
      <sensor type="ray" name="${prefix}">
        <always_on>true</always_on>
        <update_rate>20.0</update_rate>
        <pose>0 0 0 0 0 0</pose>
        <visualize>true</visualize>
        <ray>
          <scan>
            <horizontal>
              <samples>1</samples>
              <resolution>1</resolution>
              <min_angle>0</min_angle>
              <max_angle>0</max_angle>
            </horizontal>
          </scan>
          <range>
            <min>0.0160</min>
            <max>0.50</max>
            <resolution>0.1</resolution>
          </range>
        </ray>
        <plugin name="gazebo_${prefix}" filename="libcreate_light_sensor_plugin.so">
          <topicName>${topic}</topicName>
        </plugin>
      </sensor>
    </gazebo>
  </xacro:macro>
  <xacro:macro name="sim_create_light_sensors">
    <xacro:sim_create_light_sensor prefix="left_light_sensor" topic="/sim/light_left" />
    <xacro:sim_create_light_sensor prefix="left_front_light_sensor" topic="/sim/light_frontleft" />
    <xacro:sim_create_light_sensor prefix="left_center_light_sensor" topic="/sim/light_centerleft" />
    <xacro:sim_create_light_sensor prefix="right_light_sensor" topic="/sim/light_right" />
    <xacro:sim_create_light_sensor prefix="right_front_light_sensor" topic="/sim/light_frontright" />
    <xacro:sim_create_light_sensor prefix="right_center_light_sensor" topic="/sim/light_centerright" />
  </xacro:macro>

Since the light sensor are setup exactly like cliff sensors here is just the scan callback function:

static constexpr uint16_t MAX_LIGHT_VALUE = 4096;
// ...
void CreateLightRos::onScan(const ConstLaserScanStampedPtr& laser)
{
    std_msgs::UInt16 msg;

    const auto distance = laser->scan().ranges(0);

    // Check if there is no obstacle within the sensor range
    if (std::isinf(distance))
    {
        msg.data = 0;
    }
    else
    {
        const auto ratio = distance / laser->scan().range_max();
        msg.data = MAX_LIGHT_VALUE * (1.0 - ratio);
    }

    pub_queue_->push(msg, pub_);
}

The sensor value will approach the max value as objects get closer.

Since the light sensor values are apart of the create_msgs/Bumper, the bumper model plugin needs to be modified to include those values.

void CreateBumperModelRos::Load(physics::ModelPtr, sdf::ElementPtr sdf)
{
    // ...

    light_subs_[0] = nh.subscribe<std_msgs::UInt16>("/sim/light_left", 1,
                                                    boost::bind(&CreateBumperModelRos::lightSensorCallback, this,
                                                    _1, boost::ref(bumper_msg_.light_signal_left)));
    light_subs_[1] = nh.subscribe<std_msgs::UInt16>("/sim/light_frontleft", 1,
                                                    boost::bind(&CreateBumperModelRos::lightSensorCallback, this,
                                                    _1, boost::ref(bumper_msg_.light_signal_front_left)));
    light_subs_[2] = nh.subscribe<std_msgs::UInt16>("/sim/light_centerleft", 1,
                                                    boost::bind(&CreateBumperModelRos::lightSensorCallback, this,
                                                    _1, boost::ref(bumper_msg_.light_signal_center_left)));
    light_subs_[3] = nh.subscribe<std_msgs::UInt16>("/sim/light_centerright", 1,
                                                    boost::bind(&CreateBumperModelRos::lightSensorCallback, this,
                                                    _1, boost::ref(bumper_msg_.light_signal_center_right)));
    light_subs_[4] = nh.subscribe<std_msgs::UInt16>("/sim/light_frontright", 1,
                                                    boost::bind(&CreateBumperModelRos::lightSensorCallback, this,
                                                    _1, boost::ref(bumper_msg_.light_signal_front_right)));
    light_subs_[5] = nh.subscribe<std_msgs::UInt16>("/sim/light_right", 1,
                                                    boost::bind(&CreateBumperModelRos::lightSensorCallback, this,
                                                    _1, boost::ref(bumper_msg_.light_signal_right)));
}

// ...

void CreateBumperModelRos::lightSensorCallback(const std_msgs::UInt16ConstPtr& msg, uint16_t& field)
{
    field = msg->data;
}

Custom Worlds

I wanted to create a maze to have my robot navigate. The following is a maze model included in a new world file with a launch file to bring it up.

Maze Model

create_gazebo\
  models\
    maze\
      meshes\
        maze.stl
      model.sdf
      model.config
      maze.scad

The model path must be added to the export tags in package.xml:

<gazebo_ros gazebo_model_path="${prefix}/models"/>
<!-- model.config -->
<?xml version="1.0" ?>
<model>
    <name>maze</name>
    <version>1.0</version>
    <sdf version="1.6">model.sdf</sdf>
    <author>
        <name>Natesh Narain</name>
    </author>
    <description>
        Maze
    </description>
</model>
<?xml version='1.0'?>
<sdf version='1.6'>
<model name='maze'>
  <pose frame=''>0 0 0 0 0 0</pose>
  <link name='maze_link'>
    <pose frame=''>0 0 0 0 0 0</pose>
    <visual name='maze_visual'>
      <geometry>
        <mesh>
          <uri>model://maze/meshes/maze.stl</uri>
        </mesh>
      </geometry>
      <meta>
        <layer>1</layer>
      </meta>
    </visual>
    <collision name='maze_collision'>
      <geometry>
        <mesh>
          <uri>model://maze/meshes/maze.stl</uri>
        </mesh>
      </geometry>
    </collision>
    <self_collide>0</self_collide>
    <enable_wind>0</enable_wind>
    <kinematic>0</kinematic>
  </link>
  <static>1</static>
</model>
</sdf>

The collision object is built from the STL file.

I used the following OpenSCAD script to build the STL and exported it to the meshes folder:

// Units are in meters
border_height = 1;
border_size = 5;
border_thickness = 0.25;

interior_size = border_size - (2 * border_thickness);

global_offset = [-1, -1, 0];

bind = 0.01;

module wall(length, width, height) {
    cube(size=[length, width, height]);
}

module interior_walls() {
    translate([0, border_thickness + 1.5, 0])
        wall(3, border_thickness, border_height);
    translate([border_size - 3, border_thickness + 3, 0])
        wall(3, border_thickness, border_height);
}

module border() {
    difference() {
        cube(size=[border_size, border_size, border_height]);
        translate([border_thickness, border_thickness, -bind])
            cube(size=[interior_size, interior_size, border_height + 2 * bind]);
    }
}

module maze() {
    union(){
        border();
        interior_walls();
    }
}

translate(global_offset)
    maze();

World file

<!-- maze.world -->
<?xml version="1.0" ?>
<sdf version="1.5">
  <world name="maze">
    <include>
      <uri>model://sun</uri>
    </include>
    <include>
      <uri>model://ground_plane</uri>
    </include>
    <include>
        <uri>model://maze</uri>
    </include>
  </world>
</sdf>

Launch file

Re-using the gazebo.launch from above. Pass the world file as a launch file argument.

<launch>
    <arg name="verbose" default="false" />

    <include file="$(find create_gazebo)/launch/gazebo.launch">
        <arg name="world" value="$(find create_gazebo)/worlds/maze.world" />
        <arg name="verbose" value="$(arg verbose)" />
    </include>
</launch>
roslaunch create_gazebo maze.launch

Ready to start navigation.

image not found!