Integrating ROS 2 with Gazebo for Mobile Robot Simulation

Defining the Robot Model in SDF

Install the Gazebo Garden packages for ROS 2 Humble:

sudo apt install ros-humble-ros-gzgarden

Create a file named mobile_robot_env.sdf. The following configuration builds a differential drive robot equipped with GPS and IMU sensors, both configured with Gaussian noise to emulate real-world inaccuracies. A spherical coordinates plugin establishes a global reference frame for the GPS. Keyboard inputs are mapped to velocity commands using triggered publishers.

<?xml version="1.0" ?>
<sdf version="1.8">
    <world name="mobile_robot_env">
        <physics name="1ms" type="ignored">
            <max_step_size>0.001</max_step_size>
            <real_time_factor>1.0</real_time_factor>
        </physics>
        <plugin filename="gz-sim-physics-system" name="gz::sim::systems::Physics"></plugin>
        <plugin filename="gz-sim-user-commands-system" name="gz::sim::systems::UserCommands"></plugin>
        <plugin filename="gz-sim-scene-broadcaster-system" name="gz::sim::systems::SceneBroadcaster"></plugin>

        <!-- Global reference frame -->
        <spherical_coordinates>
            <surface_model>EARTH_WGS84</surface_model>
            <world_frame_orientation>ENU</world_frame_orientation>
            <latitude_deg>40.7128</latitude_deg>
            <longitude_deg>-74.0060</longitude_deg>
            <elevation>0</elevation>
            <heading_deg>0</heading_deg>
        </spherical_coordinates>

        <plugin name='gz::sim::systems::NavSat' filename='gz-sim-navsat-system'/>
        <plugin filename="gz-sim-imu-system" name="gz::sim::systems::Imu"></plugin>

        <!-- Forward motion -->
        <plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher">
            <input type="gz.msgs.Int32" topic="/input/keyboard">
                <match field="data">16777237</match>
            </input>
            <output type="gz.msgs.Twist" topic="/velocity_command">
                linear: {x: 2.5}, angular: {z: 0}
            </output>
        </plugin>
        <!-- Reverse motion -->
        <plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher">
            <input type="gz.msgs.Int32" topic="/input/keyboard">
                <match field="data">16777235</match>
            </input>
            <output type="gz.msgs.Twist" topic="/velocity_command">
                linear: {x: -2.5}, angular: {z: 0}
            </output>
        </plugin>
        <!-- Rotate left -->
        <plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher">
            <input type="gz.msgs.Int32" topic="/input/keyboard">
                <match field="data">16777234</match>
            </input>
            <output type="gz.msgs.Twist" topic="/velocity_command">
                linear: {x: 0}, angular: {z: 0.5}
            </output>
        </plugin>
        <!-- Rotate right -->
        <plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher">
            <input type="gz.msgs.Int32" topic="/input/keyboard">
                <match field="data">16777236</match>
            </input>
            <output type="gz.msgs.Twist" topic="/velocity_command">
                linear: {x: 0}, angular: {z: -0.5}
            </output>
        </plugin>
        <!-- Halt -->
        <plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher">
            <input type="gz.msgs.Int32" topic="/input/keyboard">
                <match field="data">16777220</match>
            </input>
            <output type="gz.msgs.Twist" topic="/velocity_command">
                linear: {x: 0}, angular: {z: 0}
            </output>
        </plugin>

        <light type="directional" name="sun">
            <cast_shadows>true</cast_shadows>
            <pose>0 0 10 0 0 0</pose>
            <diffuse>0.8 0.8 0.8 1</diffuse>
            <specular>0.2 0.2 0.2 1</specular>
            <attenuation>
                <range>1000</range>
                <constant>0.9</constant>
                <linear>0.01</linear>
                <quadratic>0.001</quadratic>
            </attenuation>
            <direction>-0.5 0.1 -0.9</direction>
        </light>

        <model name="ground_plane">
            <static>true</static>
            <link name="link">
                <collision name="collision">
                    <geometry><plane><normal>0 0 1</normal></plane></geometry>
                </collision>
                <visual name="visual">
                    <geometry><plane><normal>0 0 1</normal><size>100 100</size></plane></geometry>
                    <material>
                        <ambient>0.8 0.8 0.8 1</ambient>
                        <diffuse>0.8 0.8 0.8 1</diffuse>
                        <specular>0.2 0.2 0.2 1</specular>
                    </material>
                </visual>
            </link>
        </model>

        <model name='diff_bot' canonical_link='base_link'>
            <pose relative_to='world'>0 0 0 0 0 0</pose>
            <link name='base_link'>
                <pose relative_to='__model__'>0.5 0 0.4 0 0 0</pose>
                <inertial> 
                    <mass>1.14395</mass>
                    <inertia>
                        <ixx>0.00095329</ixx><ixy>0</ixy><ixz>0</ixz>
                        <iyy>0.00381317</iyy><iyz>0</iyz><izz>0.00476646</izz>
                    </inertia>
                </inertial>
                <visual name='visual'>
                    <geometry><box><size>1.8 0.8 0.4</size></box></geometry>
                    <material>
                        <ambient>1.0 0.0 0.0 1</ambient>
                        <diffuse>1.0 0.0 0.0 1</diffuse>
                        <specular>1.0 0.0 0.0 1</specular>
                    </material>
                </visual>
                <collision name='collision'>
                    <geometry><box><size>1.8 0.8 0.4</size></box></geometry>
                </collision>

                <!-- IMU sensor with Gaussian noise -->
                <sensor name="imu_device" type="imu">
                    <pose relative_to='base_link'>-0.5 0 0 0 0 0</pose>
                    <always_on>1</always_on>
                    <update_rate>50</update_rate>
                    <topic>imu_data</topic>
                    <imu>
                        <linear_acceleration>
                            <x>
                                <noise type="gaussian">
                                    <mean>0.0</mean><stddev>1.7e-2</stddev>
                                    <bias_mean>0.1</bias_mean><bias_stddev>0.001</bias_stddev>
                                </noise>
                            </x>
                            <y>
                                <noise type="gaussian">
                                    <mean>0.0</mean><stddev>1.7e-2</stddev>
                                    <bias_mean>0.1</bias_mean><bias_stddev>0.001</bias_stddev>
                                </noise>
                            </y>
                        </linear_acceleration>
                    </imu>
                </sensor>

                <!-- NavSat sensor with position noise -->
                <sensor name='gps_device' type='navsat'>
                    <always_on>true</always_on>
                    <update_rate>10</update_rate>
                    <topic>gps_data</topic>
                    <navsat>
                        <position_sensing>
                            <horizontal>
                                <noise type="gaussian">
                                    <mean>0</mean><stddev>0.00001</stddev><bias_stddev>0.00001</bias_stddev>
                                </noise>
                            </horizontal>
                            <vertical>
                                <noise type="gaussian">
                                    <mean>0</mean><stddev>0.00001</stddev><bias_stddev>0.00001</bias_stddev>
                                </noise>
                            </vertical>
                        </position_sensing>
                    </navsat>
                </sensor>
            </link>

            <link name='wheel_left'>
                <pose relative_to="base_link">-0.5 0.6 0 -1.5707 0 0</pose>
                <inertial>
                    <mass>1</mass>
                    <inertia>
                        <ixx>0.00043333</ixx><ixy>0</ixy><ixz>0</ixz>
                        <iyy>0.00043333</iyy><iyz>0</iyz><izz>0.0008</izz>
                    </inertia>
                </inertial>
                <visual name='visual'>
                    <geometry><cylinder><radius>0.4</radius><length>0.2</length></cylinder></geometry>
                    <material>
                        <ambient>0.0 0.0 1.0 1</ambient>
                        <diffuse>0.0 0.0 1.0 1</diffuse>
                        <specular>0.0 0.0 1.0 1</specular>
                    </material>
                </visual>
                <collision name='collision'>
                    <geometry><cylinder><radius>0.4</radius><length>0.2</length></cylinder></geometry>
                </collision>
            </link>

            <link name='wheel_right'>
                <pose relative_to="base_link">-0.5 -0.6 0 -1.5707 0 0</pose>
                <inertial>
                    <mass>1</mass>
                    <inertia>
                        <ixx>0.00043333</ixx><ixy>0</ixy><ixz>0</ixz>
                        <iyy>0.00043333</iyy><iyz>0</iyz><izz>0.0008</izz>
                    </inertia>
                </inertial>
                <visual name='visual'>
                    <geometry><cylinder><radius>0.4</radius><length>0.2</length></cylinder></geometry>
                    <material>
                        <ambient>0.0 0.0 1.0 1</ambient>
                        <diffuse>0.0 0.0 1.0 1</diffuse>
                        <specular>0.0 0.0 1.0 1</specular>
                    </material>
                </visual>
                <collision name='collision'>
                    <geometry><cylinder><radius>0.4</radius><length>0.2</length></cylinder></geometry>
                </collision>
            </link>

            <frame name="caster_frame" attached_to='base_link'>
                <pose>0.8 0 -0.2 0 0 0</pose>
            </frame>

            <link name='caster_wheel'>
                <pose relative_to='caster_frame'/>
                <inertial>
                    <mass>1</mass>
                    <inertia>
                        <ixx>0.00016</ixx><ixy>0</ixy><ixz>0</ixz>
                        <iyy>0.00016</iyy><iyz>0</iyz><izz>0.00016</izz>
                    </inertia>
                </inertial>
                <visual name='visual'>
                    <geometry><sphere><radius>0.2</radius></sphere></geometry>
                    <material>
                        <ambient>0.0 1 0.0 1</ambient>
                        <diffuse>0.0 1 0.0 1</diffuse>
                        <specular>0.0 1 0.0 1</specular>
                    </material>
                </visual>
                <collision name='collision'>
                    <geometry><sphere><radius>0.2</radius></sphere></geometry>
                </collision>
            </link>

            <joint name='left_axle' type='revolute'>
                <pose relative_to='wheel_left'/>
                <parent>base_link</parent>
                <child>wheel_left</child>
                <axis>
                    <xyz expressed_in='__model__'>0 1 0</xyz>
                    <limit>
                        <lower>-1.79769e+308</lower>
                        <upper>1.79769e+308</upper>
                    </limit>
                </axis>
            </joint>

            <joint name='right_axle' type='revolute'>
                <pose relative_to='wheel_right'/>
                <parent>base_link</parent>
                <child>wheel_right</child>
                <axis>
                    <xyz expressed_in='__model__'>0 1 0</xyz>
                    <limit>
                        <lower>-1.79769e+308</lower>
                        <upper>1.79769e+308</upper>
                    </limit>
                </axis>
            </joint>

            <joint name='caster_ball' type='ball'>
                <parent>base_link</parent>
                <child>caster_wheel</child>
            </joint>
        
            <!-- Differential drive controller -->
            <plugin filename="gz-sim-diff-drive-system" name="gz::sim::systems::DiffDrive">
                <left_joint>left_axle</left_joint>
                <right_joint>right_axle</right_joint>
                <wheel_separation>1.2</wheel_separation>
                <wheel_radius>0.4</wheel_radius>
                <odom_publish_frequency>50</odom_publish_frequency>
                <topic>velocity_command</topic>
                <odometry_frame>odom</odometry_frame>
                <robot_base_frame>base_footprint</robot_base_frame>
            </plugin>
        </model>
    </world>
</sdf>

Launching the Gazebo Simulation

Execute the SDF configuration to spawn the red robot within the Gazebo environment:

gz sim mobile_robot_env.sdf

Drag the Key Publisher plugin from the right-side panel into the simulation viewport to enable keyboard event broadcasting over the /input/keyboard topic. Click the play icon on the bottom-left toolbar to start the physics engine. Pressing the arrow keys will maneuver the robot, while the Enter key triggers a halt. Monitor the keystrokes or IMU messages via the Gazebo CLI:

gz topic -e -t /input/keyboard
gz topic -e -t /imu_data

Exposing Gazebo Topics to ROS 2

Native Gazebo data streams remain invisible to ROS 2 nodes unless bridged. Launch ros_gz_bridge instances to translate the simulated sensor outputs and state estimates into standard ROS 2 message formats.

Bridge the NavSat data:

ros2 run ros_gz_bridge parameter_bridge /gps_data@sensor_msgs/msg/NavSatFix@gz.msgs.NavSat

Bridge the IMU data:

ros2 run ros_gz_bridge parameter_bridge /imu_data@sensor_msgs/msg/Imu@gz.msgs.IMU

Bridge the odometry output:

ros2 run ros_gz_bridge parameter_bridge /model/diff_bot/odometry@nav_msgs/msg/Odometry@gz.msgs.Odometry

Bridge the dynamic pose information:

ros2 run ros_gz_bridge parameter_bridge /world/mobile_robot_env/dynamic_pose/info@geometry_msgs/msg/PoseArray@gz.msgs.Pose_V

Verify the availability of these converted streams within the ROS 2 ecosystem using ros2 topic list, enabling further processing or navigation logic development.

Tags: ROS2 Gazebo simulation SDF Robotics

Posted on Mon, 17 Aug 2026 16:10:12 +0000 by frosty3d