TrickFire Robotics
Gazebo

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.

plaintext
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

PackageBuild typeContains
<robot>_descriptionament_cmakeurdf/, meshes/
<robot>_bringupament_cmakelaunch/, config/ (controller YAML, RViz config)
sim_worldsament_cmakeworlds/ (SDF files), gui/ (Gazebo GUI config)
sim_commonament_pythonShared 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:

CommandSourcePurpose
move_jointssim_common/move_joints.pyPublishes a single JointTrajectory and exits. See Moving Joints.
joint_guisim_common/joint_gui.pyTkinter joint slider GUI. See Moving Joints for usage, or Joint GUI internals below for how it's implemented.
drivebasesim_common/drivebase.pySim 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 to gz_sim.launch.py as -r <world>
  • gui/gui.config and gz_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:

FieldDescription
nameRobot name, must match the <name>_description / <name>_bringup package prefix
urlOnShape assembly URL the packages were generated from
world_base_linkWhether 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:

Terminal
$ 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

Terminal
$ cd gazebo
$ colcon build --packages-select arm_description
$ source install/setup.bash

Useful 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

ArgumentDefaultDescription
rviztrueOpen RViz with the robot's config file
guitrueOpen the Joint GUI

These are set via command line:

Terminal
$ ros2 launch arm_bringup arm.launch.py gui:=false rviz:=false

Startup sequence

The launch file orchestrates several components with specific ordering dependencies:

Gazebo Simulator - starts gz_sim with the world file and GUI config

Spawn 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

Python
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

Python
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

Python
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

Python
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:

  1. joint_state_broadcaster -- reads joint states from Gazebo hardware interfaces and publishes them to /joint_states
  2. joint_trajectory_controller -- listens for JointTrajectory messages on /joint_trajectory_controller/joint_trajectory and commands Gazebo to move the joints

The controller configuration lives in config/<robot>.controller.yaml:

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
            - velocity

ROS-Gazebo bridge

Python
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:

Python
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

On this page