Gezebo Classic
Gazebo(Gazebo Classic)
This is the old version of Gazebo, which is only available on ROS1 and ROS2 Humble. (However, the old version of Gazebo is not recommended for ROS2; the new version of Gazebo is strongly advised instead.)
ROS2's legacy Gazebo is only available in the Humble release and has been removed starting from Jazzy and later versions.
(If you don't want to learn the old version of Gazebo, skip directly to the next section on Ignition Gazebo.)
The difference between these two isn't that big for us—it's mainly just that one has more tutorials and the other has fewer. You can go ahead and learn the older version of Gazebo; it works just the same.
(Beginners can also just learn the more tutorial-rich Gazebo Classic to avoid some detours.)
Gezebo official website:

If you want to migrate from the old version of Gazebo to the new version of Gazebo, please refer to the official tutorial below:
Migrating from Gazebo Classic to Ignition Gazebo (Gazebo Fortress) on ROS2 Humble:
https://gazebosim.org/docs/fortress/gazebo\_classic\_migration/
Migrating from Gazebo Classic to Gazebo Sim (Gazebo Harmonic) on ROS2 Jazzy
Versions after Jazzy should not change too much. You can also temporarily refer to the tutorial below (or find the corresponding version's tutorial on the official website):
https://gazebosim.org/docs/harmonic/gazebo\_classic\_migration/
Gazebo Classic Installation and Running
ROS2 only has the old version of Gazebo in Humble.
sudo apt install ros-humble-gazebo-ros ros-humble-gazebo-ros-pkgs
Once the installation is complete, we can launch Gazebo and load the ROS2 plugin using the following command line.
gazebo --verbose -s libgazebo_ros_init.so -s libgazebo_ros_factory.so
Seeing the log and Gazebo interface below, if there are no major issues, it means success. (Some warnings indicate that Gazebo Classic has been deprecated, and the use of the new Ignition Gazebo is encouraged; these warnings can be ignored.)
root@Dell-G15-5511:/home/tungchiahui/UserFolder/MySource/ROS_WS/ROS2_WS/6.ws_simulations$ gazebo --verbose -s libgazebo_ros_init.so -s libgazebo_ros_factory.so
Gazebo multi-robot simulator, version 11.10.2
Copyright (C) 2012 Open Source Robotics Foundation.
Released under the Apache 2 License.
http://gazebosim.org
[Msg] Waiting for master.
Gazebo multi-robot simulator, version 11.10.2
Copyright (C) 2012 Open Source Robotics Foundation.
Released under the Apache 2 License.
http://gazebosim.org
[Wrn] [gazebo_ros_init.cpp:178]
# # ####### ####### ### ##### #######
## # # # # # # # #
# # # # # # # # #
# # # # # # # # #####
# # # # # # # # #
# ## # # # # # # #
# # ####### # ### ##### #######
This version of Gazebo, now called Gazebo classic, reaches end-of-life
in January 2025. Users are highly encouraged to migrate to the new Gazebo
using our migration guides (https://gazebosim.org/docs/latest/gazebo_classic_migration?utm_source=gazebo_ros_pkgs&utm_medium=cli)
[Msg] Waiting for master.
[Msg] Connected to gazebo master @ http://127.0.0.1:11345
[Msg] Publicized address: 192.168.31.60
[Msg] Loading world file [/usr/share/gazebo-11/worlds/empty.world]
XDG_RUNTIME_DIR (/run/user/1000) is not owned by us (uid 0), but by uid 1000! (This could e.g. happen if you try to connect to a non-root PulseAudio as a root user, over the native protocol. Don't do that.)
ALSA lib pcm_dmix.c:1032:(snd_pcm_dmix_open) unable to open slave
AL lib: (EE) ALCplaybackAlsa_open: Could not open playback device 'default': No such file or directory
[Err] [OpenAL.cc:84] Unable to open audio device[default]
Audio will be disabled.
[Msg] Connected to gazebo master @ http://127.0.0.1:11345
[Msg] Publicized address: 192.168.31.60
[Wrn] [GuiIface.cc:298] Couldn't locate specified .ini. Creating file at "/root/.gazebo/gui.ini"
[Wrn] [GuiIface.cc:120] QStandardPaths: runtime directory '/run/user/1000' is not owned by UID 0, but a directory permissions 0700 owned by UID 1000 GID 1000
[Wrn] [Event.cc:61] Warning: Deleting a connection right after creation. Make sure to save the ConnectionPtr from a Connect call
libcurl: (35) error:0A000126:SSL routines::unexpected eof while reading
[Wrn] [ModelDatabase.cc:212] Unable to connect to model database using [http://models.gazebosim.org//database.config]. Only locally installed models will be available.
[Wrn] [Event.cc:61] Warning: Deleting a connection right after creation. Make sure to save the ConnectionPtr from a Connect call

Introduction to Plugins and Node Services
Use the previous command to start Gazebo and load the gazebo_ros plugin. Use the following command to view the plugin's nodes and the services that this node provides to us.
Node List
ros2 node list

Then let's take a look at what services this node provides externally.
ros2 service list

Apart from the services related to parameters, we can see three other special services:
- /spawn_entity, used to load models into Gazebo.
- /get_model_list, used to retrieve the list of models
- /delete_entity, used to delete a model that has already been loaded in Gazebo.
We want Gazebo to display our configured robot model. Simply use /spawn_entity to load it.
Next, we can request the service to load the model. First, let me show you the service's interface type.
ros2 service type /spawn_entity

ros2 interface show gazebo_msgs/srv/SpawnEntity

You can see that the service request content includes:
- string name, the name of the entity to load (optional).
- string xml, the XML description string of the entity, either URDF or SDF.
- The
robot_namespacegenerates the namespace for the robot and all ROS interfaces, which is very useful in multi-robot simulation. - geometry_msgs/Pose initial_pose, the robot's initial position
- string reference_frame, the initial pose is defined relative to the frame of this entity. If set to "empty", "world", or "map", Gazebo's world frame is used. If a non-existent entity is specified, an error will be returned.
Call the service to load the model.
Our tutorial here uses the fishbot model from Yuxiang ROS: https://github.com/fishros/fishbot/blob/navgation2/src/fishbot\_description/urdf/fishbot\_gazebo.urdf
<?xml version="1.0"?>
<robot name="fishbot">
<link name="base_footprint"/>
<joint name="base_joint" type="fixed">
<parent link="base_footprint"/>
<child link="base_link"/>
<origin xyz="0.0 0.0 0.076" rpy="0 0 0"/>
</joint>
<link name="base_link">
<visual>
<origin xyz="0 0 0.0" rpy="0 0 0"/>
<geometry>
<cylinder length="0.12" radius="0.10"/>
</geometry>
<material name="blue">
<color rgba="0.1 0.1 1.0 0.5" />
</material>
</visual>
<collision>
<origin xyz="0 0 0.0" rpy="0 0 0"/>
<geometry>
<cylinder length="0.12" radius="0.10"/>
</geometry>
<material name="blue">
<color rgba="0.1 0.1 1.0 0.5" />
</material>
</collision>
<inertial>
<mass value="0.2"/>
<inertia ixx="0.0122666" ixy="0" ixz="0" iyy="0.0122666" iyz="0" izz="0.02"/>
</inertial>
</link>
<link name="laser_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<cylinder length="0.02" radius="0.02"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 0.5" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<cylinder length="0.02" radius="0.02"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 0.5" />
</material>
</collision>
<inertial>
<mass value="0.1"/>
<inertia ixx="0.000190416666667" ixy="0" ixz="0" iyy="0.0001904" iyz="0" izz="0.00036"/>
</inertial>
</link>
<joint name="laser_joint" type="fixed">
<parent link="base_link" />
<child link="laser_link" />
<origin xyz="0 0 0.075" />
</joint>
<link name="imu_link">
<visual>
<origin xyz="0 0 0.0" rpy="0 0 0"/>
<geometry>
<box size="0.02 0.02 0.02"/>
</geometry>
</visual>
<collision>
<origin xyz="0 0 0.0" rpy="0 0 0"/>
<geometry>
<box size="0.02 0.02 0.02"/>
</geometry>
</collision>
<inertial>
<mass value="0.1"/>
<inertia ixx="0.000190416666667" ixy="0" ixz="0" iyy="0.0001904" iyz="0" izz="0.00036"/>
</inertial>
</link>
<joint name="imu_joint" type="fixed">
<parent link="base_link" />
<child link="imu_link" />
<origin xyz="0 0 0.02" />
</joint>
<link name="left_wheel_link">
<visual>
<origin xyz="0 0 0" rpy="1.57079 0 0"/>
<geometry>
<cylinder length="0.04" radius="0.032"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 0.5" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.57079 0 0"/>
<geometry>
<cylinder length="0.04" radius="0.032"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 0.5" />
</material>
</collision>
<inertial>
<mass value="0.2"/>
<inertia ixx="0.000190416666667" ixy="0" ixz="0" iyy="0.0001904" iyz="0" izz="0.00036"/>
</inertial>
</link>
<link name="right_wheel_link">
<visual>
<origin xyz="0 0 0" rpy="1.57079 0 0"/>
<geometry>
<cylinder length="0.04" radius="0.032"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 0.5" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.57079 0 0"/>
<geometry>
<cylinder length="0.04" radius="0.032"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 0.5" />
</material>
</collision>
<inertial>
<mass value="0.2"/>
<inertia ixx="0.000190416666667" ixy="0" ixz="0" iyy="0.0001904" iyz="0" izz="0.00036"/>
</inertial>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link" />
<child link="left_wheel_link" />
<origin xyz="-0.02 0.10 -0.06" />
<axis xyz="0 1 0" />
</joint>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link" />
<child link="right_wheel_link" />
<origin xyz="-0.02 -0.10 -0.06" />
<axis xyz="0 1 0" />
</joint>
<link name="caster_link">
<visual>
<origin xyz="0 0 0" rpy="1.57079 0 0"/>
<geometry>
<sphere radius="0.016"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 0.5" />
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.57079 0 0"/>
<geometry>
<sphere radius="0.016"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 0.5" />
</material>
</collision>
<inertial>
<mass value="0.02"/>
<inertia ixx="0.000190416666667" ixy="0" ixz="0" iyy="0.0001904" iyz="0" izz="0.00036"/>
</inertial>
</link>
<joint name="caster_joint" type="fixed">
<parent link="base_link" />
<child link="caster_link" />
<origin xyz="0.06 0.0 -0.076" />
<axis xyz="0 1 0" />
</joint>
<gazebo reference="caster_link">
<material>Gazebo/Black</material>
</gazebo>
<gazebo reference="caster_link">
<mu1 value="0.0"/>
<mu2 value="0.0"/>
<kp value="1000000.0" />
<kd value="10.0" />
</gazebo>
<gazebo>
<plugin name='diff_drive' filename='libgazebo_ros_diff_drive.so'>
<ros>
<namespace>/</namespace>
<remapping>cmd_vel:=cmd_vel</remapping>
<remapping>odom:=odom</remapping>
</ros>
<update_rate>30</update_rate>
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.2</wheel_separation>
<wheel_diameter>0.065</wheel_diameter>
<max_wheel_torque>20</max_wheel_torque>
<max_wheel_acceleration>1.0</max_wheel_acceleration>
<publish_odom>true</publish_odom>
<publish_odom_tf>true</publish_odom_tf>
<publish_wheel_tf>false</publish_wheel_tf>
<odometry_frame>odom</odometry_frame>
<robot_base_frame>base_footprint</robot_base_frame>
</plugin>
<plugin name="fishbot_joint_state" filename="libgazebo_ros_joint_state_publisher.so">
<ros>
<remapping>~/out:=joint_states</remapping>
</ros>
<update_rate>30</update_rate>
<joint_name>right_wheel_joint</joint_name>
<joint_name>left_wheel_joint</joint_name>
</plugin>
</gazebo>
<gazebo reference="laser_link">
<material>Gazebo/Black</material>
</gazebo>
<gazebo reference="imu_link">
<sensor name="imu_sensor" type="imu">
<plugin filename="libgazebo_ros_imu_sensor.so" name="imu_plugin">
<ros>
<namespace>/</namespace>
<remapping>~/out:=imu</remapping>
</ros>
<initial_orientation_as_reference>false</initial_orientation_as_reference>
</plugin>
<always_on>true</always_on>
<update_rate>100</update_rate>
<visualize>true</visualize>
<imu>
<angular_velocity>
<x>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>2e-4</stddev>
<bias_mean>0.0000075</bias_mean>
<bias_stddev>0.0000008</bias_stddev>
</noise>
</x>
<y>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>2e-4</stddev>
<bias_mean>0.0000075</bias_mean>
<bias_stddev>0.0000008</bias_stddev>
</noise>
</y>
<z>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>2e-4</stddev>
<bias_mean>0.0000075</bias_mean>
<bias_stddev>0.0000008</bias_stddev>
</noise>
</z>
</angular_velocity>
<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>
<z>
<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>
</z>
</linear_acceleration>
</imu>
</sensor>
</gazebo>
<gazebo reference="laser_link">
<sensor name="laser_sensor" type="ray">
<always_on>true</always_on>
<visualize>true</visualize>
<update_rate>5</update_rate>
<pose>0 0 0.075 0 0 0</pose>
<ray>
<scan>
<horizontal>
<samples>360</samples>
<resolution>1.000000</resolution>
<min_angle>0.000000</min_angle>
<max_angle>6.280000</max_angle>
</horizontal>
</scan>
<range>
<min>0.120000</min>
<max>3.5</max>
<resolution>0.015000</resolution>
</range>
<noise>
<type>gaussian</type>
<mean>0.0</mean>
<stddev>0.01</stddev>
</noise>
</ray>
<plugin name="laserscan" filename="libgazebo_ros_ray_sensor.so">
<ros>
<remapping>~/out:=scan</remapping>
</ros>
<output_type>sensor_msgs/LaserScan</output_type>
<frame_name>laser_link</frame_name>
</plugin>
</sensor>
</gazebo>
</robot>
By now, you're probably eager to start typing commands to load our robot into Gazebo. But hold on—Xiaoyu also recommends a visual service request tool. In fact, in Chapter 6, Xiaoyu introduced a tool in the rqt toolkit called the Service Caller.
Type rqt in the command line, then select Services -> Service Caller from the plugin menu. Next, choose the /spawn_entity service from the dropdown menu, and you will see the interface below.

Next, we copy and paste the URDF model of our FishBot into the XML (remember to delete the original ''!).

Then click the call button in the upper right corner, and you should see the following image indicating success.

Next, you can see the factory report that the robot was successfully created and sent into Gazebo.
Now, looking at our Gazebo, a small, white robot has appeared.

Shift + left-click, or simply clicking the middle mouse wheel, both allow you to drag the view. Many students who have played third-person games are surely familiar with this kind of control.
Loading multiple robots at different positions
You can produce another fishbot (for those who need multi-robot simulation later).
Modify the parameters in rqt, add a namespace, then change a position so that the second robot spawns 1 meter away from the first one, and click Call.

Returned successfully. Now drag and observe in Gazebo, and you'll see an additional robot has appeared, positioned exactly 1 meter (one small grid cell equals one meter) along the X-axis (red).

Query and delete robot
Using the rqt tool, we will also make requests to the other two service interfaces.
First, check how many models are in the simulation environment.

Found three models: one for the ground, one for fishbot, and one for fishbot_0.
Let's try deleting fishbot_0 next. Select Delete Entity, enter the name fishbot_0, and pick up the little phone to notify the factory to recycle our No. 0 fishbot.

The call succeeded. Observe the robot in Gazebo — the person is gone.

Create a workspace
The code in this section references FishROS: https://github.com/fishros/fishbot/tree/navgation2
# 创建工作空间
mkdir -p ws_simulations/src
cd ws_simulations/src
Create a function package
ros2 pkg create fishbot_description --build-type ament_cmake
cd fishbot_description
Then configure package.xml
<exec_depend>rviz2</exec_depend>
<exec_depend>xacro</exec_depend>
<exec_depend>robot_state_publisher</exec_depend>
<exec_depend>joint_state_publisher</exec_depend>
<exec_depend>ros2launch</exec_depend>

Then modify CMakeLists.txt
install(
DIRECTORY launch urdf rviz meshes
DESTINATION share/${PROJECT_NAME}
)

Clone the FishROS repository and copy the files inside it to our directory. These are all from the previous section and have nothing to do with this section's learning, so just copy them directly.

Open the workspace, add a gazebo.launch.py file in src/fishbot_description/launch, and start writing a launch file to load the robot model in Gazebo.
We mainly need to do two things:
- To launch Gazebo, we can write the command line as a launch node.
ExecuteProcess(
cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so', gazebo_world_path],
output='screen')
- Above, we loaded the robot by directly copying the URDF in XML format, which is very inconvenient. We can use a node called
spawn_entity.pyprovided by gazebo_ros, which supports generating a robot directly into Gazebo from a file path.
This node requires two parameters: a robot model name and the URDF file path. This is straightforward — earlier, we used package_share to construct the URDF path.
spawn_entity_cmd = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-entity', robot_name_in_model, '-file', urdf_model_path ], output='screen')
First, load Teacher Zhao Xuzuo's template.

We first need ExecuteProcess to input terminal commands, so we should uncomment from launch.actions import ExecuteProcess first.
To make it easier to modify the loaded robot model and URDF, we also need to use the share directory. Therefore, we should uncomment the line from ament_index_python.packages import get_package_share_directory and import os.
The completed launch file is as follows:
from launch import LaunchDescription
from launch_ros.actions import Node
# 封装终端指令相关类
from launch.actions import ExecuteProcess
# from launch.substitutions import FindExecutable
# 参数声明与获取
# from launch.actions import DeclareLaunchArgument
# from launch.substitutions import LaunchConfiguration
# 文件包含相关
# from launch.actions import IncludeLaunchDescription
# from launch.launch_description_sources import PythonLaunchDescriptionSource
# 分组相关
# from launch_ros.actions import PushRosNamespace
# from launch.actions import GroupAction
# 事件相关
# from launch.event_handlers import OnProcessStart,OnProcessExit
# from launch.actions import ExecuteProcess,RegisterEventHandler,LogInfo
# 获取功能包下share目录或路径
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
robot_name_in_model = 'fishbot'
package_name = 'fishbot_description'
urdf_name = "fishbot_gazebo.urdf"
pkg_share = get_package_share_directory(f"{package_name}")
urdf_model_path = os.path.join(pkg_share, f'urdf/urdf/{urdf_name}')
# Start Gazebo server
start_gazebo_cmd = ExecuteProcess(
cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so'],
output='screen')
# Launch the robot
spawn_entity_cmd = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-entity', robot_name_in_model, '-file', urdf_model_path ], output='screen')
return LaunchDescription([start_gazebo_cmd,spawn_entity_cmd])
Compile and run
colcon build --packages-select fishbot_description
source install/setup.bash
ros2 launch fishbot_description gazebo.launch.py

plugin
Use the following command to view all dynamic link libraries:
ls /opt/ros/humble/lib/libgazebo_ros*

/opt/ros/humble/lib/libgazebo_ros2_control.so
/opt/ros/humble/lib/libgazebo_ros_ackermann_drive.so
/opt/ros/humble/lib/libgazebo_ros_bumper.so
/opt/ros/humble/lib/libgazebo_ros_camera.so
/opt/ros/humble/lib/libgazebo_ros_diff_drive.so
/opt/ros/humble/lib/libgazebo_ros_elevator.so
/opt/ros/humble/lib/libgazebo_ros_factory.so
/opt/ros/humble/lib/libgazebo_ros_force.so
/opt/ros/humble/lib/libgazebo_ros_force_system.so
/opt/ros/humble/lib/libgazebo_ros_ft_sensor.so
/opt/ros/humble/lib/libgazebo_ros_gps_sensor.so
/opt/ros/humble/lib/libgazebo_ros_hand_of_god.so
/opt/ros/humble/lib/libgazebo_ros_harness.so
/opt/ros/humble/lib/libgazebo_ros_imu_sensor.so
/opt/ros/humble/lib/libgazebo_ros_init.so
/opt/ros/humble/lib/libgazebo_ros_joint_pose_trajectory.so
/opt/ros/humble/lib/libgazebo_ros_joint_state_publisher.so
/opt/ros/humble/lib/libgazebo_ros_node.so
/opt/ros/humble/lib/libgazebo_ros_p3d.so
/opt/ros/humble/lib/libgazebo_ros_planar_move.so
/opt/ros/humble/lib/libgazebo_ros_projector.so
/opt/ros/humble/lib/libgazebo_ros_properties.so
/opt/ros/humble/lib/libgazebo_ros_ray_sensor.so
/opt/ros/humble/lib/libgazebo_ros_state.so
/opt/ros/humble/lib/libgazebo_ros_template.so
/opt/ros/humble/lib/libgazebo_ros_tricycle_drive.so
/opt/ros/humble/lib/libgazebo_ros_utils.so
/opt/ros/humble/lib/libgazebo_ros_vacuum_gripper.so
/opt/ros/humble/lib/libgazebo_ros_video.so
/opt/ros/humble/lib/libgazebo_ros_wheel_slip.so
Motion control plugin + odometry odom
In this lesson, we configure the two-wheel differential control plugin to get our robot moving.

Plugin Introduction
Gazebo is software independent of ROS and provides a rich set of APIs for external use. Gazebo plugins can be roughly divided into two types based on their purpose:
- A plugin for control, through which you can control the joint motion of a robot, including position, velocity, and force control, such as the two-wheel differential controller covered in this lesson.
- A plugin for data acquisition, such as an IMU sensor for capturing the robot's inertia, and a LiDAR for collecting point cloud information around the robot.
Of course, the two types of plugin functions mentioned above can also be written into a single plugin. The two-wheel differential drive plugin (gazebo_ros_diff_drive) is an enhanced two-in-one version. (Differential control + odom)
The two-wheel differential plugin is used to control the position changes of the robot's wheel joints. At the same time, the plugin also retrieves feedback on the wheel's position and velocity. Based on the feedback position information combined with the kinematic model, the current robot's pose (odometry) can be calculated.
The two-wheel differential controller can send the target wheel speeds to Gazebo and retrieve the actual speeds and positions from Gazebo. (Note: the target speed is sent to Gazebo, while the actual speed is fed back. Target ≠ actual; for example, if a wheel is stuck, no matter what target speed you send, the actual speed will be 0.)
The quickest way to understand a system's functionality is to look at its external inputs and outputs. Without further explanation, see the diagram below:

The diagram above summarizes the input and output information of gazebo_ros_diff_drive. It clearly shows that this plugin primarily takes control commands as input and outputs odometry information. Next, Xiaoyu will guide you through understanding both the input and output parts.
This plugin requires configuring a series of parameters as shown below:
I don't know if you still remember in Chapter 7, Xiaoyu's introduction to the forward kinematics of a two-wheel differential drive chassis. To complete the forward and inverse kinematics of the chassis and odometry estimation, you must know the wheel diameter and wheelbase.
Additionally, this plugin provides some options for controlling the output. Since it's a simulation, you also need to tell the plugin the corresponding joint names for the wheels and other information, resulting in the parameter table below:
| Configuration item | Meaning |
|---|---|
| ros | ROS-related configuration, including namespaces and topic remapping, among others. |
| update_rate | Data update rate |
| left_joint | Left wheel joint name |
| right_joint | Right wheel joint name |
| wheel_separation | Distance between the left and right wheels |
| wheel_diameter | The diameter of the wheel |
| max_wheel_torque | The wheel's maximum torque |
| max_wheel_acceleration | The maximum acceleration of the wheel |
| publish_odom | Publish odometry |
| publish_odom_tf | Whether to publish the odometry tf switch |
| publish_wheel_tf | Switch for publishing wheel tf data |
| odometry_frame | The framed ID of the odometer is ultimately reflected in the topic and TF. |
| robot_base_frame | The robot's base frame ID |
Control command: The two-wheel differential controller subscribes to the topic cmd_vel by default to obtain the target linear and angular velocities. The type of this topic is: geometry_msgs/msg/Twist.
This interface mainly includes linear and angular velocities in x, y, and z, representing the corresponding velocities in the three directions of the coordinate system. (For detailed interface content, please refer to the Hardware Platform chapter.)
After receiving this topic data, the two-wheel differential controller converts the angular velocity and linear velocity into the rotational speeds of the two wheels and sends them to Gazebo.
I am sorry, I cannot process this request as it does not contain any Simplified Chinese Markdown fragment to translate. Please provide the text you would like me to translate.
The default output topic for odometry information is odom, and its message type is: nav_msgs/msg/Odometry
Its data mainly consists of three parts: (see the Hardware Platform chapter for detailed interface content)
- header, indicating the time this message was posted
- pose, indicating the current robot position and orientation
- twist, representing the current robot's linear and angular velocity
- The data also includes a covariance, which represents the covariance matrix. I'll write a post about it later, but for now, just understand what it means.
Odometry TF information can also be output: set it to true, and after subscribing to the tf topic, you will see a message like the one below. It is recommended to manually modify it later after configuration to compare the differences.
- header:
stamp:
sec: 6157
nanosec: 907000000
frame_id: odom
child_frame_id: base_footprint
transform:
translation:
x: 0.0005557960241049835
y: -0.0007350446303238693
z: 0.01599968753145574
rotation:
x: 4.691143395208505e-07
y: 7.115496626557812e-06
z: -0.018531475772549166
w: 0.9998282774331005
The wheel TF information can also be output: set it to true, and when subscribing to the TF topic, you will see a message like the one below. It is recommended to manually modify it after configuration to compare the differences.
- header:
stamp:
sec: 6157
nanosec: 941000000
frame_id: base_link
child_frame_id: left_wheel_link
transform:
translation:
x: -0.02
y: 0.1
z: -0.06
rotation:
x: 0.0
y: 0.049519025127821005
z: 0.0
w: 0.9987731805321918
- header:
stamp:
sec: 6157
nanosec: 941000000
frame_id: base_link
child_frame_id: right_wheel_link
transform:
translation:
x: -0.02
y: -0.1
z: -0.06
rotation:
x: 0.0
y: -0.0663387077034509
z: 0.0
w: 0.9977971616817898
The fishbot URDF from FishROS that we downloaded earlier already includes the differential drive plugin content, as shown below:
Because this is a plugin for Gazebo, in URDF, we need to configure it using <gazebo>. Since we are configuring a plugin for gazebo, we need to add the plugin sub-plugin under the gazebo tag.
<gazebo>
<plugin name='diff_drive' filename='libgazebo_ros_diff_drive.so'>
<ros>
<namespace>/</namespace>
<remapping>cmd_vel:=cmd_vel</remapping>
<remapping>odom:=odom</remapping>
</ros>
<update_rate>30</update_rate>
<left_joint>left_wheel_joint</left_joint>
<right_joint>right_wheel_joint</right_joint>
<wheel_separation>0.2</wheel_separation>
<wheel_diameter>0.065</wheel_diameter>
<max_wheel_torque>20</max_wheel_torque>
<max_wheel_acceleration>1.0</max_wheel_acceleration>
<publish_odom>true</publish_odom>
<publish_odom_tf>true</publish_odom_tf>
<publish_wheel_tf>true</publish_wheel_tf>
<odometry_frame>odom</odometry_frame>
<robot_base_frame>base_footprint</robot_base_frame>
</plugin>
</gazebo>
Compile it:
colcon build
source install/setup.bash
ros2 launch fishbot_description gazebo.launch.py
Then you can take a look at these two commands:
ros2 node list
ros2 topic list

We can now see the plugin's subscribed /cmd_vel and published /odom.
Then we can control the fishbot by publishing cmd_vel through the teleop-twist-keyboard node.
sudo apt install ros-humble-teleop-twist-keyboard
Use the following nodes to control
ros2 run teleop_twist_keyboard teleop_twist_keyboard
Then try using it to control the robot's movement.
U I O
J K L
M < >
Click, and you'll see the fishbot zooming around in Gazebo. Then open the terminal, print the odom topic and tf topic, and move the robot to observe the data changes.
You can also use rqt to display speed data.
rqt
Select Plugin -> Visualization -> Plot

In the Topic field above, enter /cmd_vel/linear/x, then enter /cmd_vel/angular/z, and use the keyboard to control the robot's movement.


You can also view the odom in rviz2.
rviz2
Press U on the keyboard control node to make the robot spin in circles.

Although the robot's trajectory is already displayed in RVIZ, the robot model itself is not visible, nor can you see the wheels turning. Let's walk through how to fix this together.
Earlier we introduced that the node we use to publish the robot model is robot_state_publisher, so we add this node to gazebo.launch.py, along with the launch node for rviz2. The final gazebo.launch.py content is as follows:
from launch import LaunchDescription
from launch_ros.actions import Node
# 封装终端指令相关类
from launch.actions import ExecuteProcess
# from launch.substitutions import FindExecutable
# 参数声明与获取
# from launch.actions import DeclareLaunchArgument
# from launch.substitutions import LaunchConfiguration
# 文件包含相关
# from launch.actions import IncludeLaunchDescription
# from launch.launch_description_sources import PythonLaunchDescriptionSource
# 分组相关
# from launch_ros.actions import PushRosNamespace
# from launch.actions import GroupAction
# 事件相关
# from launch.event_handlers import OnProcessStart,OnProcessExit
# from launch.actions import ExecuteProcess,RegisterEventHandler,LogInfo
# 获取功能包下share目录或路径
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
robot_name_in_model = 'fishbot'
package_name = 'fishbot_description'
urdf_name = "fishbot_gazebo.urdf"
pkg_share = get_package_share_directory(f"{package_name}")
urdf_model_path = os.path.join(pkg_share, f'urdf/urdf/{urdf_name}')
# gazebo_world_path = os.path.join(pkg_share, 'world/fishbot.world')
# Start Gazebo server
# start_gazebo_cmd = ExecuteProcess(
# cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so',gazebo_world_path],
# output='screen')
start_gazebo_cmd = ExecuteProcess(
cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so'],
output='screen')
# Launch the robot
spawn_entity_cmd = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-entity', robot_name_in_model, '-file', urdf_model_path ], output='screen')
# Start Robot State publisher
start_robot_state_publisher_cmd = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
arguments=[urdf_model_path]
)
# Launch RViz
start_rviz_cmd = Node(
package='rviz2',
executable='rviz2',
name='rviz2',
output='screen',
# arguments=['-d', default_rviz_config_path]
)
return LaunchDescription([start_gazebo_cmd,spawn_entity_cmd,start_robot_state_publisher_cmd,start_rviz_cmd])
Save, compile, and launch
colcon build
ros2 launch fishbot_description gazebo.launch.py
Now there's a model in rviz2.

You can save the rviz2 configuration.


Then add the rviz2 path configuration in the launch file:
default_rviz_config_path = os.path.join(pkg_share, 'rviz/rviz2.rviz')
# Launch RViz
start_rviz_cmd = Node(
package='rviz2',
executable='rviz2',
name='rviz2',
output='screen',
arguments=['-d', default_rviz_config_path]
)
Here is the complete launch:
from launch import LaunchDescription
from launch_ros.actions import Node
# 封装终端指令相关类
from launch.actions import ExecuteProcess
# from launch.substitutions import FindExecutable
# 参数声明与获取
# from launch.actions import DeclareLaunchArgument
# from launch.substitutions import LaunchConfiguration
# 文件包含相关
# from launch.actions import IncludeLaunchDescription
# from launch.launch_description_sources import PythonLaunchDescriptionSource
# 分组相关
# from launch_ros.actions import PushRosNamespace
# from launch.actions import GroupAction
# 事件相关
# from launch.event_handlers import OnProcessStart,OnProcessExit
# from launch.actions import ExecuteProcess,RegisterEventHandler,LogInfo
# 获取功能包下share目录或路径
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
robot_name_in_model = 'fishbot'
package_name = 'fishbot_description'
urdf_name = "fishbot_gazebo.urdf"
pkg_share = get_package_share_directory(f"{package_name}")
urdf_model_path = os.path.join(pkg_share, f'urdf/urdf/{urdf_name}')
# gazebo_world_path = os.path.join(pkg_share, 'world/fishbot.world')
default_rviz_config_path = os.path.join(pkg_share, 'rviz/rviz2.rviz')
# Start Gazebo server
# start_gazebo_cmd = ExecuteProcess(
# cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so',gazebo_world_path],
# output='screen')
start_gazebo_cmd = ExecuteProcess(
cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so'],
output='screen')
# Launch the robot
spawn_entity_cmd = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-entity', robot_name_in_model, '-file', urdf_model_path ], output='screen')
# Start Robot State publisher
start_robot_state_publisher_cmd = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
arguments=[urdf_model_path]
)
# Launch RViz
start_rviz_cmd = Node(
package='rviz2',
executable='rviz2',
name='rviz2',
output='screen',
arguments=['-d', default_rviz_config_path]
)
return LaunchDescription([start_gazebo_cmd,spawn_entity_cmd,start_robot_state_publisher_cmd,start_rviz_cmd])
You can compile and test it yourself.
Inertial Measurement Unit (IMU)
In the previous lesson, by configuring the two-wheel differential controller, we successfully got fishbot moving in Gazebo. In this lesson, we will configure the IMU sensor plugin in fishbot's URDF to get the IMU module working.
An inertial measurement unit is a device that measures an object's three-axis attitude angles (or angular rates) and acceleration. Generally, an IMU contains three single-axis accelerometers and three single-axis gyroscopes. The accelerometers detect the acceleration signals of the object along the three independent axes of the body coordinate system, while the gyroscopes detect the angular velocity signals of the body relative to the navigation coordinate system. By measuring the angular velocity and acceleration of the object in three-dimensional space, the device calculates the object's attitude. It has significant application value in navigation.
The passage above was excerpted by Xiaoyu from an encyclopedia. One key point you need to know is that an IMU can measure the following three sets of data:
- Three-axis accelerometer acceleration
- Three-dimensional gyroscope angular velocity
- 3D magnetometer (some models do not include a magnetometer)
Algorithms such as six-axis, nine-axis, or others can output three-axis Euler angles (Yaw, Pitch, Roll), which can then be converted into quaternions.
What does an IMU look like? Just go find the control group in person and ask them—they often work with these.
The cheap ones look like this (MPU6050, MPU9050, etc.):
MPU6050 is six-axis, and MPU9050 is nine-axis.

The expensive ones look like this (HWT101CT, HWT605, etc.):
Among them, the HWT101CT is three-axis, while the HWT605 is six-axis. (Each has its own advantages and disadvantages.)

What does the free one look like?

Simulations are free, haha. Next, let's configure the simulation's IMU.
The message type corresponding to IMU is sensor_msgs/msg/Imu.
ROS IMU data only includes three-axis acceleration, angular velocity, and quaternions. (There is no magnetometer or Euler angles. The magnetometer is not required, and Euler angles and quaternions are interchangeable. Quaternions are better suited for algorithmic computation, so quaternions are used.)
For details on the IMU interface, please refer to the hardware platform section.
You can see that in addition to the three covariances corresponding to each data point, each one also corresponds to a covariance matrix for 3*3.
With the experience from the last lesson, we can easily add an IMU sensor. However, there is one thing to keep in mind: to simulate the IMU sensor more realistically, we need to add some noise to our simulated IMU sensor.
Add what? Add some Gaussian noise. Gaussian noise only requires specifying two parameters: the mean and standard deviation. However, due to the unique nature of IMU sensors, we also need to add two bias parameters to the model: 平均值偏差 and 标准差偏差.
For a more in-depth introduction to Gazebo simulation and noise models, refer to these two posts from Yuxiang ROS:
- Advanced Gazebo Simulation Tutorial: Sensor Gaussian Noise (Part 1)
- Advanced Gazebo Simulation Tutorial: Sensor Gaussian Noise (Part 2)
Below is the URDF configuration code for the IMU sensor. You can refer to the corresponding section in the post to understand it. The plugin library corresponding to the IMU is libgazebo_ros_imu_sensor.so:
<gazebo reference="imu_link">
<sensor name="imu_sensor" type="imu">
<plugin filename="libgazebo_ros_imu_sensor.so" name="imu_plugin">
<ros>
<namespace>/</namespace>
<remapping>~/out:=imu</remapping>
</ros>
<initial_orientation_as_reference>false</initial_orientation_as_reference>
</plugin>
<always_on>true</always_on>
<update_rate>100</update_rate>
<visualize>true</visualize>
<imu>
<angular_velocity>
<x>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>2e-4</stddev>
<bias_mean>0.0000075</bias_mean>
<bias_stddev>0.0000008</bias_stddev>
</noise>
</x>
<y>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>2e-4</stddev>
<bias_mean>0.0000075</bias_mean>
<bias_stddev>0.0000008</bias_stddev>
</noise>
</y>
<z>
<noise type="gaussian">
<mean>0.0</mean>
<stddev>2e-4</stddev>
<bias_mean>0.0000075</bias_mean>
<bias_stddev>0.0000008</bias_stddev>
</noise>
</z>
</angular_velocity>
<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>
<z>
<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>
</z>
</linear_acceleration>
</imu>
</sensor>
</gazebo>
The fishbot we downloaded earlier already includes this content, so there's no need to add it again. Just run it directly.
ros2 launch fishbot_description gazebo.launch.py
ros2 topic list

ros2 topic info /imu
ros2 topic echo /imu


header:
stamp:
sec: 150
nanosec: 599000000
frame_id: base_footprint
orientation:
x: 3.434713830866392e-07
y: 7.119913105768616e-06
z: -0.00028312437320413914
w: 0.9999999598948884
orientation_covariance:
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
angular_velocity:
x: -0.00013597855247901325
y: 0.0006306135617081868
z: -0.00015794894627685146
angular_velocity_covariance:
- 4.0e-08
- 0.0
- 0.0
- 0.0
- 4.0e-08
- 0.0
- 0.0
- 0.0
- 4.0e-08
linear_acceleration:
x: 0.08679200038530369
y: 0.07753419258567491
z: 9.687910969061628
linear_acceleration_covariance:
- 0.00028900000000000003
- 0.0
- 0.0
- 0.0
- 0.00028900000000000003
- 0.0
- 0.0
- 0.0
- 0.00028900000000000003
Visualizing with rqt:

Laser Radar
In this section, we will get to know a new sensor that is widely used in applications such as autonomous driving and indoor navigation. For example, robotic vacuum cleaners use it as an important tool for perceiving the environment. This sensor is the LiDAR.
LiDAR (Light Detection And Ranging), abbreviated as LiDAR, also called laser in English, translates to — laser detection and ranging.
The principle of LiDAR is also very simple, much like a bat's method of localization—everyone knows how bats locate, right? It's echolocation, as shown below.

A typical single-line LiDAR has one transmitter and one receiver. The transmitter emits a laser beam toward a target in front, the object reflects the laser back, and the LiDAR's receiver detects the reflected laser.

By calculating the time interval between sending and receiving, and multiplying it by the speed of light, the distance the laser travels can be determined. This calculation method is known as TOF (Time of Flight).
Besides TOF, there are other methods for distance measurement, such as triangulation. We won't go into detail here, but here's a post for you to read on your own: Detailed Explanation of Laser Triangulation Ranging Principles
Currently, almost all LiDAR sensors on the market use triangulation ranging, such as those from SLAMTEC:

It is important to note that although there is only one transmitter and one receiver, the LiDAR can rotate via a motor, enabling it to measure distances in a 360-degree range around its surroundings.
A five-digit number looks like this:

A four-digit one looks like this (we have one).

Three-digit ones look like this (we have them too).

Two-digit numbers look like this

Here's the free one—it looks like this.
Simulated, and free of charge.

Because LiDAR is a type of ray-based sensor, such sensors are encapsulated as a dynamic library in the Gazebo plugin libgazebo_ros_ray_sensor.so.
Next, let's look at the LiDAR topic message interface sensor_msgs/msg/LaserScan.
The data structure of the radar is somewhat complex, but with the help of comments and naming conventions, you should be able to understand most of it. It's okay if you don't fully grasp it — under normal circumstances, we don't directly manipulate the radar data.
The radar model does not need collision. Please delete it, otherwise it will block the laser emission.
With the experience from before, we just need to add the following content to the URDF. However, since we downloaded the version already set up by FishROS, there's no need to modify it.
<gazebo reference="laser_link">
<sensor name="laser_sensor" type="ray">
<always_on>true</always_on>
<visualize>true</visualize>
<update_rate>10</update_rate>
<pose>0 0 0.075 0 0 0</pose>
<ray>
<scan>
<horizontal>
<samples>360</samples>
<resolution>1.000000</resolution>
<min_angle>0.000000</min_angle>
<max_angle>6.280000</max_angle>
</horizontal>
</scan>
<range>
<min>0.120000</min>
<max>3.5</max>
<resolution>0.015000</resolution>
</range>
<noise>
<type>gaussian</type>
<mean>0.0</mean>
<stddev>0.01</stddev>
</noise>
</ray>
<plugin name="laserscan" filename="libgazebo_ros_ray_sensor.so">
<ros>
<remapping>~/out:=scan</remapping>
</ros>
<output_type>sensor_msgs/LaserScan</output_type>
<frame_name>laser_link</frame_name>
</plugin>
</sensor>
</gazebo>
You can see:
- The radar can also set the update frequency
update_rate, which is set to 5 here. - The radar can have its resolution set. When set to 1, with 360 sampling points, the final number of generated point cloud points is 360.
- Radar also has noise, with the model being
gaussian. - The radar has a scanning range of
range. Here, it is configured as 0.12–3.5 with a resolution of 0.015. - The
poseof the radar is the position setting value in the radar's joint.
Below is the blue-colored coverage area of the LiDAR's laser beams:

ros2 topic list

ros2 topic info /scan
ros2 topic echo /scan

Next, we try using rviz2 to visualize the LiDAR data.
Add and modify RVIZ2 as follows: (Laser data can be viewed through the LaserScan plugin)

I believe that even after you make these changes, you still won't see any LiDAR data. On the contrary, the data echoed from the topic will either be 0 or inf (infinity). If you look at Gazebo, you'll notice that the LiDAR hasn't reached any object.

So we can manually add some objects around the LiDAR. Click the cube, sphere, or cylinder in the Gazebo toolbar and place a few of them within the LiDAR's maximum scanning radius.



Ultrasonic
This thing is not very significant for ROS2 algorithms. The control group can implement it directly on the MCU. If needed, you can study this section. (You can skip this section)
In actual robot development, we might use an ultrasonic sensor to implement real-time obstacle avoidance, since ultrasonic sensors are much cheaper than LiDAR (a cheap one costs just a few dollars).
So in this section, we'll discuss how to use ROS2 + Gazebo to simulate an ultrasonic sensor.
Here's a paragraph for the encyclopedia entry:
A microcontroller is a compact integrated circuit designed to govern a specific operation in an embedded system. It typically includes a processor, memory, and input/output peripherals on a single chip. Microcontrollers are widely used in automatically controlled products and devices, such as automobile engine control systems, remote controls, office machines, appliances, power tools, and toys. By reducing size and cost compared to a design that uses a separate microprocessor, memory, and input/output devices, microcontrollers make it economical to digitally control even more devices and processes.
An ultrasonic sensor is a sensor that converts ultrasonic signals into other forms of energy signals (typically electrical signals). Ultrasound is a mechanical wave with a vibration frequency higher than 20 kHz. It features high frequency, short wavelength, minimal diffraction, and especially good directivity, allowing it to propagate directionally as a ray. Ultrasound has strong penetrating ability through liquids and solids, particularly in solids that are opaque to light. When ultrasound encounters impurities or interfaces, it produces significant reflections, forming echo signals, and when it hits moving objects, it can generate the Doppler effect. Ultrasonic sensors are widely used in industries, national defense, biomedicine, and other fields.
Next, let's take a look at what it looks like:

The cheaper ones look like this, with two heads in total—one for transmitting waves and one for receiving waves. This one is slightly more advanced, featuring a photoresistor that can provide some compensation for the ultrasonic data.
What is the principle of an ultrasonic sensor?

距离=(发送时间-接收时间)*速度/2Copy to clipboardErrorCopied
After looking at the principle of ultrasonic sensors, you may have noticed it's the same as the LiDAR sensor we covered earlier. That's right, so the ultrasonic sensor plugin and the LiDAR sensor plugin are the same in Gazebo plugins:
libgazebo_ros_ray_sensor.so
Ultrasound always needs to be mounted somewhere on the robot, so we first add a joint and a Joint. To keep things simple, we'll just write a name for the link. If needed, you can add it following the approach from the previous chapters.
<link name="ultrasonic_sensor_link" />
<joint name="ultrasonic_sensor_joint" type="fixed">
<parent link="base_link"/>
<child link="ultrasonic_sensor_link"/>
<origin xyz="0.07 0.0 0.076" rpy="0 0 0"/>
</joint>
Once the joints have been added, we can configure the Gazebo plugins. The Gazebo plugin configuration is as follows:
<gazebo reference="ultrasonic_sensor_link">
<sensor type="ray" name="ultrasonic_sensor">
<pose>0 0 0 0 0 0</pose>
<visualize>true</visualize>
<update_rate>5</update_rate>
<ray>
<scan>
<horizontal>
<samples>5</samples>
<resolution>1</resolution>
<min_angle>-0.12</min_angle>
<max_angle>0.12</max_angle>
</horizontal>
<vertical>
<samples>5</samples>
<resolution>1</resolution>
<min_angle>-0.01</min_angle>
<max_angle>0.01</max_angle>
</vertical>
</scan>
<range>
<min>0.02</min>
<max>4</max>
<resolution>0.01</resolution>
</range>
<noise>
<type>gaussian</type>
<mean>0.0</mean>
<stddev>0.01</stddev>
</noise>
</ray>
<plugin name="ultrasonic_sensor_controller" filename="libgazebo_ros_ray_sensor.so">
<ros>
<remapping>~/out:=ultrasonic_sensor_1</remapping>
</ros>
<output_type>sensor_msgs/Range</output_type>
<radiation_type>ultrasound</radiation_type>
<frame_name>ultrasonic_sensor_link</frame_name>
</plugin>
</sensor>
</gazebo>
Once added, you can compile and test the code.
colcon build --packages-select fishbot_description
source install/setup.bash
ros2 launch fishbot_description gazebo.launch.py
You can place something in front of an object that isn't there.

Open the terminal and enter the following command:
ros2 topic list
ros2 topic info /ultrasonic_sensor_1
ros2 topic echo /ultrasonic_sensor_1Copy to clipboardErrorCopied
You should be able to see the data below.
header:
stamp:
sec: 4458
nanosec: 1000000
frame_id: ultrasonic_sensor_link
radiation_type: 0
field_of_view: 0.23999999463558197
min_range: 0.019999999552965164
max_range: 4.0
range: 2.6798219680786133
Here, range is the distance from the fishbot to the wall: 2.67982
Let's talk about the data types of ultrasonic sensors sensor_msgs/msg/Range.
# ros2 topic info /ultrasonic_sensor_1
Type: sensor_msgs/msg/Range
Publisher count: 1
Subscription count: 0
You can see a detailed explanation using ros2 interface show sensor_msgs/msg/Range. Let's translate it.
# Single range reading from an active ranger that emits energy and reports
# one range reading that is valid along an arc at the distance measured.
# This message is not appropriate for laser scanners. See the LaserScan
# message if you are working with a laser scanner.
#
# This message also can represent a fixed-distance (binary) ranger. This
# sensor will have min_range===max_range===distance of detection.
# These sensors follow REP 117 and will output -Inf if the object is detected
# and +Inf if the object is outside of the detection range.
std_msgs/Header header # timestamp in the header is the time the ranger
# returned the distance reading
# Radiation type enums
# If you want a value added to this list, send an email to the ros-users list
uint8 ULTRASOUND=0
uint8 INFRARED=1
uint8 radiation_type # 传感器射线类型
# (sound, IR, etc) [enum]
float32 field_of_view # 距离数据对应的弧[rad]的大小,测量物体的范围介于
# -field_of_view/2 到 field_of_view/2 之间。
# 0 角度对应于传感器的 x 轴。
float32 min_range # 最小范围值 [m]
float32 max_range # 最大范围值 [m]
# 固定距离需要 min_range==max_range
float32 range # 范围数据 [m]
# (Note: values < range_min or > range_max should be discarded)
# Fixed distance rangers only output -Inf or +Inf.
# -Inf represents a detection within fixed distance.
# (Detection too close to the sensor to quantify)
# +Inf represents no detection within the fixed distance.
# (Object out of range)
In conclusion, focus primarily on the range.
Add ultrasonic data in rviz2
Add ->By topic->Range


Building a world map
In this section, we will set up a test environment in Gazebo. It's actually quite simple — we can just use Gazebo's wall-drawing tool to get it done.
The term "world" refers to the environment, and Gazebo's world file is used to describe the world model, i.e., the environment model.
Gazebo already provides us with many commonly used object models. In addition to basic shapes like spheres, cylinders, and cubes, it also includes airplanes, cars, houses, and other things you may not have in real life.
However, when you first install Gazebo, these models are not downloaded automatically. You need to download them manually. Find a folder where you want to store the models, open a terminal, and copy-paste the following command:
git clone https://github.com/osrf/gazebo_models
Add the path where the model is stored to ~/.bashrc (the part after the second colon can be omitted; what comes after the second colon is multiple paths.)
GAZEBO_MODEL_PATH is a macro from the older Gazebo Classic.
IGN_GAZEBO_RESOURCE_PATH is a macro in the new version of Ignition Gazebo.
The models are all universal, so they can be configured together.

Refresh environment variables
source ~/.bashrc
Now open the terminal again, enter gazebo, and switch the tab to Insert.

Under the Insert tab, you can see a directory along with the model names listed within it. As the download script continues to download, more and more models will appear here.
Just drag a few over and build a beautiful environment~
Every successful man has a car, and we are no exception.

Above are the open-source models that Gazebo has prepared for us. We can also use Gazebo's tools to draw our own environment.
Then you can also use the wall tool to build walls.
Gazebo top-left corner -> Edit -> Building Editor
Next, you can see an editing interface like this.

Click the "Wall" button on the left, and you can start building walls in the white area above. Unlike The Sims, where walls are drawn directly in 3D, this tool draws 2D walls that generate 3D walls.

After building, you can also select Add Color or Add Texture, then click on the wall below to add color or texture to it.
First, you need a map. Xiaoyu has prepared two for you, both images are 800x600 pixels.


Open Gazebo -> In the top-left corner of Gazebo -> Edit -> Building Editor -> Select Import at the bottom left.

Save the two images above to your local machine. On this screen, select the images, and remember to choose Next.

The size correspondence on the left

We choose the default, 100 pixels/meter. Click OK (you'll need to manually change the 100 before you can click OK), and then you can use the image to draw walls.
Note: After importing the image, the wall won't appear directly. The image only provides the approximate location of the wall, so you'll need to manually trace the edges again using the wall tool.

After building, click File -> Exit, and select Exit in the exit dialog box.
Next, you can see the wall in the Gazebo interface. By manually adding a few more objects, it will be ready for the navigation tasks below.

After adding, click File -> Save World to save the file under our fishbot_description world directory.
If you don't have a world directory yet, you can create one manually first.

Loading a world is actually quite simple. You can either launch Gazebo first and then manually load the file, or load it directly when starting Gazebo:
For example, load fishbot.world on top of the previously loaded ROS2 plugin.
gazebo --verbose -s libgazebo_ros_init.so -s libgazebo_ros_factory.so 你的world文件目录/fishbot.world
Modify the launch file by writing the above command line into gazebo.launch.py.
gazebo_world_path = os.path.join(pkg_share, 'world/fishbot.world')
# Start Gazebo server
start_gazebo_cmd = ExecuteProcess(
cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so', gazebo_world_path],
output='screen')
Here is the entire launch file:
from launch import LaunchDescription
from launch_ros.actions import Node
# 封装终端指令相关类
from launch.actions import ExecuteProcess
# from launch.substitutions import FindExecutable
# 参数声明与获取
# from launch.actions import DeclareLaunchArgument
# from launch.substitutions import LaunchConfiguration
# 文件包含相关
# from launch.actions import IncludeLaunchDescription
# from launch.launch_description_sources import PythonLaunchDescriptionSource
# 分组相关
# from launch_ros.actions import PushRosNamespace
# from launch.actions import GroupAction
# 事件相关
# from launch.event_handlers import OnProcessStart,OnProcessExit
# from launch.actions import ExecuteProcess,RegisterEventHandler,LogInfo
# 获取功能包下share目录或路径
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
robot_name_in_model = 'fishbot'
package_name = 'fishbot_description'
urdf_name = "fishbot_gazebo.urdf"
world_name = "fishbot.world"
pkg_share = get_package_share_directory(f"{package_name}")
urdf_model_path = os.path.join(pkg_share, f'urdf/urdf/{urdf_name}')
gazebo_world_path = os.path.join(pkg_share, f'world/{world_name}')
default_rviz_config_path = os.path.join(pkg_share, 'rviz/rviz2.rviz')
# Start Gazebo server
start_gazebo_cmd = ExecuteProcess(
cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so',gazebo_world_path],
output='screen')
# Launch the robot
spawn_entity_cmd = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-entity', robot_name_in_model, '-file', urdf_model_path ], output='screen')
# Start Robot State publisher
start_robot_state_publisher_cmd = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
arguments=[urdf_model_path]
)
# Launch RViz
start_rviz_cmd = Node(
package='rviz2',
executable='rviz2',
name='rviz2',
output='screen',
arguments=['-d', default_rviz_config_path]
)
return LaunchDescription([start_gazebo_cmd,spawn_entity_cmd,start_robot_state_publisher_cmd,start_rviz_cmd])
Finally, remember to modify the CMakeLists.txt file so that the world file is copied to the install directory after compilation.

colcon build
source install/setup.bash
ros2 launch fishbot_description gazebo.launch.py


Framework optimization
The code for this section has also been posted to the repository by the senior. If you need it, please check: https://github.com/tungchiahui/ROS\_WS/tree/main/ROS2\_WS%2F6.ws\_simulations%2Fsrc%2Ffishbot\_description
For example, support optimizations like xacro.
First, split the content of the original fishbot_gazebo.urdf into several URDF and Xacro files, then use a main fishbot.urdf.xacro to reference them. (Anyone who has learned Xacro will know how to do this.)

The optimized launch is as follows:
from launch import LaunchDescription
from launch_ros.actions import Node
# 封装终端指令相关类
from launch.actions import ExecuteProcess
# from launch.substitutions import FindExecutable
# 参数声明与获取
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
# 文件包含相关
# from launch.actions import IncludeLaunchDescription
# from launch.launch_description_sources import PythonLaunchDescriptionSource
# 分组相关
# from launch_ros.actions import PushRosNamespace
# from launch.actions import GroupAction
# 事件相关
# from launch.event_handlers import OnProcessStart,OnProcessExit
# from launch.actions import ExecuteProcess,RegisterEventHandler,LogInfo
# 获取功能包下share目录或路径
from ament_index_python.packages import get_package_share_directory
from launch_ros.parameter_descriptions import ParameterValue
from launch.substitutions import Command,LaunchConfiguration
import os
def generate_launch_description():
robot_name_in_model = 'fishbot'
package_name = 'fishbot_description'
# urdf_xacro_name = "fishbot_gazebo.urdf"
world_name = "fishbot.world"
pkg_share = get_package_share_directory(f"{package_name}")
urdf_xacro_model_path = os.path.join(pkg_share, "urdf/xacro","fishbot.urdf.xacro")
gazebo_world_path = os.path.join(pkg_share, f'world/{world_name}')
default_rviz_config_path = os.path.join(pkg_share, 'rviz/rviz2.rviz')
model = DeclareLaunchArgument(name="model", default_value=urdf_xacro_model_path)
robot_description = ParameterValue(Command(["xacro ",LaunchConfiguration("model")]))
# Start Gazebo server
start_gazebo_cmd = ExecuteProcess(
cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so',gazebo_world_path],
output='screen')
# Launch the robot
spawn_entity_cmd = Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=['-entity', robot_name_in_model, '-topic', '/robot_description' ], output='screen')
# Start Robot State publisher
start_robot_state_publisher_cmd = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{"robot_description": robot_description}]
)
# Launch RViz
start_rviz_cmd = Node(
package='rviz2',
executable='rviz2',
name='rviz2',
output='screen',
arguments=['-d', default_rviz_config_path]
)
return LaunchDescription([model,start_gazebo_cmd,spawn_entity_cmd,start_robot_state_publisher_cmd,start_rviz_cmd])
colcon build
source install/setup.bash
ros2 launch fishbot_description gazebo.launch.py


Then you can go ahead and work on navigation! You can also continue learning the new version of Ignition Gazebo, but there are very few tutorials for it—as of 2024, only Teacher Zhao Xuzuo has covered it. ** Recommended to learn the new Gazebo Harmonic (the default version for ROS2 Jazzy) **