Gazebo Fortress physics validation¶
The colcon tests validate the controllers on mock_components hardware — they
prove the artifact loads, the interfaces bind, the law computes the right
command, and the chain wires up. They do not prove the controller
stabilizes anything, because mock hardware has no dynamics.
The kontrolem_gazebo package closes that gap with real physics: it brings
up the cart–double-inverted-pendulum in Gazebo Fortress (Ignition Gazebo 6)
and lets an exported artifact balance it. Unlike the Isaac Sim path,
this runs headless in CI — no GPU required — so it doubles as an automated
stabilization test.
Architecture¶
ign gazebo (physics @ 1 kHz, sim time)
└─ gz_ros2_control/GazeboSimSystem (hardware plugin in the gazebo URDF)
└─ controller_manager ← runs INSIDE the sim process, on sim time
├─ joint_state_broadcaster
└─ lqr | lqg | hinf controller → effort commands
/clock ──(ros_gz_bridge)──▶ ROS 2 side (use_sim_time)
The gz_ros2_control system plugin embeds the controller manager inside the
Gazebo process on sim time, so there is no separate ros2_control_node. The
Gazebo URDF variant swaps the hardware plugin mock_components/GenericSystem →
gz_ros2_control/GazeboSimSystem; the controller config and the exported
artifact are byte-for-byte the same ones the mock-hardware demos use.
Run it¶
colcon build --packages-up-to kontrolem_gazebo
source install/setup.bash
# LQR (default), headless server:
ros2 launch kontrolem_gazebo gz_bringup.launch.py
# with the Gazebo GUI:
ros2 launch kontrolem_gazebo gz_bringup.launch.py gui:=true
# LQG / H-infinity from the same launch:
ros2 launch kontrolem_gazebo gz_bringup.launch.py controller:=lqg
ros2 launch kontrolem_gazebo gz_bringup.launch.py controller:=hinf
Watch it hold with ros2 topic echo /joint_states — the top link (joint2) is
passive, so it can only stay upright if the actuated cart_joint/joint1
are actively stabilizing it.
Test¶
colcon test --packages-select kontrolem_gazebo
colcon test-result --verbose
The launch test runs Gazebo headless, waits for the controller to load its
exported artifact, then samples /joint_states for several seconds and asserts
every joint stays inside the linearization validity region (|q| < 0.3) — i.e.
the plant is being stabilized, not toppling. All three controllers (LQR, LQG,
H∞) stabilize the pendulum from their exported bundles.
Notes / gotchas¶
These were hard-won; they live in the package README.md too.
Fortress binary. The launch calls
ign gazebo(Ignition 6). On systems where onlygz simexists, adjust the launch file.System-plugin path. Fortress locates system plugins via
IGN_GAZEBO_SYSTEM_PLUGIN_PATH(notLD_LIBRARY_PATH), and sourcing ROS does not set it, solibgz_ros2_control-system.so(in/opt/ros/$ROS_DISTRO/lib) is not found by default. The launch injects it viaadditional_envon theign gazeboprocess — a top-levelSetEnvironmentVariabledoes not reliably reach that subprocess.Effort command interface. The actuated joints expose
effortcommand interfaces;gz_ros2_controlapplies them directly, matching the controllers’command_interface: effort.Dynamic compensators. As everywhere, LQG/H∞ set
update_rateexplicitly equal tocontroller_manager.update_rate(the Tustin discretization rate).Log-scraping the activation. A launch test keys on the controller’s own
on_configurelog line (loaded <method> artifact …), not the ros2_control spawner’sConfigured and activated <name>banner: the spawner hardcodes ANSI color around the controller name, which splits that substring, and rcutils logs to stderr rather than stdout.