Architecture
How the gazebo/ colcon workspace is laid out, how sim gazebo builds it, and how the launch files orchestrate simulation startup.
gazebo/ at the repo root is a standard ROS 2 colcon workspace. Every robot, plus a couple of shared packages, lives directly under it as a colcon package.
gazebo/
├── <robot>_description/ # URDF + meshes from OnShape
├── <robot>_bringup/ # arm launch file + configs
├── sim_common/ # shared Python nodes & launch helpers
├── sim_worlds/ # world SDFs + Gazebo GUI config
├── build/ ┐
├── install/ ├─ generated by colcon, gitignored
└── log/ ┘Two packages per robot
Every robot splits into a <robot>_description package containing the URDF and meshes.
Then a <robot>_bringup package with the launch file and configs.
Package types
| Package | Build type | Contains |
|---|---|---|
<robot>_description | ament_cmake | urdf/, meshes/ |
<robot>_bringup | ament_cmake | launch/, config/ (controller YAML, RViz config) |
sim_worlds | ament_cmake | worlds/ (SDF files), gui/ (Gazebo GUI config) |
sim_common | ament_python | Shared launch helpers and standalone ROS 2 nodes |
The difference between ament_cmake and ament_python packages is in its project managers.
ament_cmake uses
CMake
with the CMakeLists.txt file while ament_python uses Setup
Tools with the
setup.cfg file.
sim_common
This is a shared package containing cross-package repetitive code & also independent ROS nodes like the joint GUI. Its setup.py registers three console scripts:
| Command | Source | Purpose |
|---|---|---|
move_joints | sim_common/move_joints.py | Publishes a single JointTrajectory and exits. See Moving Joints. |
joint_gui | sim_common/joint_gui.py | Tkinter joint slider GUI. See Moving Joints for usage, or Joint GUI internals below for how it's implemented. |
drivebase | sim_common/drivebase.py | Sim equivalent of the real rover's drivebase node - converts joystick deflection messages into wheel velocity commands, mirroring urc-2023/src/drivebase. |
It also exports sim_common/launch_utils.py, a library (not a node) of helpers every <robot>.launch.py imports - get_asset() for resolving files in a package's share directory, plus node/launch-action builders for the Gazebo server, spawning, controllers, and the ROS-Gazebo bridge. See launch_utils.py below for what each helper does.
sim_worlds
Holds the environment the robot spawns into:
worlds/empty.world.sdf- the default world, passed togz_sim.launch.pyas-r <world>gui/gui.configandgz_gui.xml- the Gazebo GUI layout (camera view, panels)
Its share/ install path is added to GZ_SIM_RESOURCE_PATH at launch time so Gazebo can find world assets - see the SIM_WORLDS_SHARE handling in cli/gazebo/launch.py.
robots.json
The repo-root robots.json is the CLI's registry of available robots. For each one the object includes these fields:
| Field | Description |
|---|---|
name | Robot name, must match the <name>_description / <name>_bringup package prefix |
url | OnShape assembly URL the packages were generated from |
world_base_link | Whether base_link is fixed to the world |
New entries are added automatically by sim gazebo create - see Adding a New Robot.
Building
sim gazebo <robot_name> (see Running Gazebo) drives colcon for you, under the hood it uses a command like this:
$ colcon build --packages-up-to <robot>_bringup <robot>_description sim_worlds sim_common \
--cmake-args -DBUILD_TESTING=OFF--packages-up-to builds only the requested robot's packages plus their dependencies (not every robot in the workspace), so switching robots doesn't force a full rebuild. By default it also deletes build/, install/, and log/ first - equivalent to running sim gazebo clean - for a clean rebuild every launch; pass --no-build to skip building entirely and reuse the existing install/.
Building a single package by hand
$ cd gazebo
$ colcon build --packages-select arm_description
$ source install/setup.bashUseful when iterating on one package without going through the CLI. Remember to source install/setup.bash afterwards - ROS 2 won't find the package otherwise. See Dev Notes for more.
build/, install/, log/ are generated
All three are gitignored. Don't edit anything inside them - they're wiped on every build. If
colcon behaves strangely after unrelated changes, delete them and rebuild with sim gazebo clean.
Launch system
Each robot has a launch file at gazebo/<robot>_bringup/launch/<robot>.launch.py that orchestrates the full simulation startup. These are the commands the sim CLI does under the hood.
Flags
| Argument | Default | Description |
|---|---|---|
rviz | true | Open RViz with the robot's config file |
gui | true | Open the Joint GUI |
These are set via command line:
$ ros2 launch arm_bringup arm.launch.py gui:=false rviz:=falseStartup sequence
The launch file orchestrates several components with specific ordering dependencies:
gz_sim with the world file and GUI configSpawn Robot + Robot State Publisher (parallel) - spawns the URDF model into Gazebo and
starts robot_state_publisher with the URDF
Joint State Broadcaster - starts reading joint states from Gazebo once the spawn finishes.
Joint Trajectory Controller - accepts trajectory commands and drives joints, once the broadcaster is ready.
ROS-Gazebo Bridge, RViz, Joint GUI (all in parallel) - bridges /clock between Gazebo
and ROS, opens RViz with the saved config, and opens the Joint GUI with the URDF file.
The robot spawn uses a TimerAction with a 5-second delay to give Gazebo time to fully initialize before the model is spawned. Steps 3 and 4 use RegisterEventHandler with OnProcessExit to enforce ordering -- the trajectory controller can't start until the state broadcaster is ready, and the broadcaster can't start until the robot is spawned.
Components
Gazebo simulation
gz_sim = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(get_package_share_directory("ros_gz_sim"), "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": " ".join(["-r", world_file, "--gui-config", gz_gui_config])
}.items(),
)Launches Gazebo Harmonic with:
-r-- start running immediately (not paused)- The world SDF file from
sim_worlds(default:empty.world.sdf) - A custom GUI config from
sim_worlds/gui/gui.config
URDF processing
robot_desc = xacro.process_file(
urdf_file,
mappings={"controller_config": controller_config},
).toxml()The URDF is processed through xacro at launch time. The controller_config mapping passes the path to the controller YAML so the control xacro can reference it. The result is a fully resolved URDF string used for both spawning and state publishing.
Robot spawn
spawn_robot = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "arm", "-string", robot_desc, "-x", "0", "-y", "0", "-z", "0.1"],
)Spawns the robot into the Gazebo world from the processed URDF string. The -z 0.1 offset prevents the robot from spawning inside the ground plane.
Robot state publisher
robot_state_publisher = Node(
package="robot_state_publisher",
executable="robot_state_publisher",
parameters=[{"robot_description": robot_desc}],
)Publishes the robot's TF tree and makes the URDF available on the /robot_description topic. RViz uses this to render the robot model.
Controllers
Two controllers are spawned in sequence:
joint_state_broadcaster-- reads joint states from Gazebo hardware interfaces and publishes them to/joint_statesjoint_trajectory_controller-- listens forJointTrajectorymessages on/joint_trajectory_controller/joint_trajectoryand commands Gazebo to move the joints
The controller configuration lives in config/<robot>.controller.yaml:
controller_manager:
ros__parameters:
use_sim_time: true
update_rate: 60 # Hz
joint_trajectory_controller:
ros__parameters:
joints:
- shoulder_1
- elbow_1
- wrist_1
- wrist_2
command_interfaces:
- position
state_interfaces:
- position
- velocityROS-Gazebo bridge
bridge = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=["/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock"],
)Bridges the /clock topic from Gazebo to ROS 2. This is essential for use_sim_time: true -- without it, ROS nodes won't have synchronized time with the simulation.
RViz
Launches RViz2 with a saved config file. Conditional on the rviz launch argument.
Joint GUI
Launches the Tkinter joint control GUI with the URDF file path as an argument. Conditional on the gui launch argument. See Moving Joints for how to use it, or Joint GUI internals below for how it's implemented.
launch_utils.py
The sim_common package provides a get_asset() helper used throughout launch files:
from sim_common.launch_utils import get_asset
controller_config = get_asset("arm_bringup", "config", "arm.controller.yaml")It resolves a file path inside a ROS 2 package's share directory and exits with an error if the file doesn't exist. This catches missing files early rather than failing mid-launch.
Writing a custom launch file
If you need to customize a robot's launch beyond what sim gazebo create generates, edit gazebo/<robot>_bringup/launch/<robot>.launch.py. The file is standard ROS 2 launch Python - you can add nodes, change parameters, or modify the startup sequence.
Common customizations:
- Adding sensor bridges (cameras, lidar)
- Changing spawn position
- Adding additional ROS 2 nodes
- Modifying controller parameters