ROS 2 Control#
Drive a robot in NVIDIA Isaac Sim with the standard ros2_control stack running inside the simulation. The isaacsim.ros2.control extension hosts a ros2_control Controller Manager in-process and presents the simulated robot as a hardware interface, so the controllers, YAML, and command-line tools you would use on real hardware work against the simulation.
This is the in-process counterpart to the bridge-based MoveIt workflow in MoveIt 2, where MoveIt drives the arm over the Isaac Sim ROS 2 bridge, exchanging joint states and commands on ROS 2 topics. Hosting the real Controller Manager inside the simulation runs the controllers next to physics, removes the topic round-trip between the controller and the robot, and mirrors how you bring up a physical robot. To move to hardware later, you swap the hardware interface and reuse the same controllers and YAML.
If ros2_control is new to you: it is the standard ROS 2 control framework, where a Controller Manager loads controllers that read and write a robot’s hardware interfaces. See ros2_control for the full picture.
Learning objectives#
You will:
Install the demo package and its dependencies.
Drive a UR10 with MoveIt 2 through the in-process Controller Manager.
Command the arm directly, without MoveIt.
See how a robot’s USD drives map to
ros2_controlinterfaces, so you can apply the extension to your own robot, add sensors, and run several robots at once.
Quick check without a workspace#
Before the full MoveIt walkthrough, you can confirm the extension works with a single command. This needs only Isaac Sim, not a ROS 2 workspace or the packages below. The script builds a one-joint arm, starts the in-process Controller Manager, activates a broadcaster and a trajectory controller, commands the joint, and checks that it moves. Run from the NVIDIA Isaac Sim root:
./python.sh standalone_examples/api/isaacsim.ros2.control/ros2_control_smoke_test.py
It prints DEMO SMOKE TEST: PASS and exits 0 once the joint reaches its target. Add --headless to run without a viewport.
Prerequisites#
Complete ROS 2 Installation so your ROS 2 workspace is set up. The
isaac_ros2_control_demopackage and the supportingros2_controlpackages ship with your NVIDIA Isaac Sim download, inside the workspace (for examplehumble_wsorjazzy_ws).Install the controller plugins and the UR10 MoveIt configuration:
sudo apt install ros-$ROS_DISTRO-ros2-controllers ros-$ROS_DISTRO-ur-moveit-config
The RViz motion-planning panel also uses
ros-$ROS_DISTRO-rviz-visual-tools. It is optional: without it the panel logs a harmless plugin error and planning still works.Build the demo package and source the install. The standard clone path from ROS 2 Installation is
~/IsaacSim-ros_workspaces/${ROS_DISTRO}_ws; substitute the path you used:cd ~/IsaacSim-ros_workspaces/${ROS_DISTRO}_ws colcon build --packages-select isaac_ros2_control_demo source install/setup.bash
On multiple machines, set
FASTRTPS_DEFAULT_PROFILES_FILEas described in ROS 2 Installation before launching Isaac Sim and in every terminal that sends or receives ROS 2 messages.
Run the UR10 with MoveIt 2#
You will start the simulation, bring up MoveIt 2, then plan and execute a motion that the UR10 follows in the viewport.
Terminal 1. Start Isaac Sim with the standalone example. The script opens a UR10 USD, builds an Action Graph with
ROS2 Publish ClockandROS2ControlManageragainstur10_controllers.yaml, and presses Play. Run from the NVIDIA Isaac Sim root:./python.sh standalone_examples/api/isaacsim.ros2.control/ur10_ros2_control_demo.pyWait until the viewport opens and the UR10 is visible. The Controller Manager is created on the first physics tick after Play, so by the time the UR10 is visible it is normally up. First-run asset download can add up to a minute.
Note
At Info log verbosity the extension prints
[ros2.control] backend readyto stdout once it finishes loading. That is an earlier signal than the UR10 appearing, and it does not mean the Controller Manager exists yet; the CM comes up on the first physics tick after Play.Terminal 2. Source the workspace and launch MoveIt 2:
source ~/IsaacSim-ros_workspaces/${ROS_DISTRO}_ws/install/setup.bash ros2 launch isaac_ros2_control_demo ur10_in_process.launch.py
This starts
move_group,robot_state_publisher, and RViz with the motion-planning panel, and activates the controllers against the in-process Controller Manager. Confirm both controllers are active:ros2 control list_controllers
Expected: the table lists
joint_state_broadcasterandscaled_joint_trajectory_controller, each showingactivein the State column.Note
The launch first waits up to 60 seconds for Isaac Sim to publish
/robot_description, then brings up MoveIt and activates the controllers, so start Terminal 1 first and wait for the UR10 to appear. If it times out, confirm Isaac Sim is running and at Play.In RViz, plan and execute a motion with the MotionPlanning panel. Set a goal pose (drag the interactive marker, or pick
<random_valid>under Goal State), click Plan, then Execute. The trajectory goes to the in-process Controller Manager, and the UR10 moves.Expected: RViz shows the planned trajectory, then the UR10 in the viewport follows it in real time.
The UR10 following a MoveIt-planned trajectory in the NVIDIA Isaac Sim viewport (left), driven from RViz (right).#
How it works#
The extension is driven by a small Action Graph: an On Playback Tick node wired into a ROS2ControlManager node.
When useSimTime is enabled, add a ROS2 Publish Clock node or another ROS clock source to provide /clock. On
the first tick after Play, the ROS2ControlManager node reads the articulation under targetPrim, builds a URDF
from its drive configuration, and starts the Controller Manager against your controller YAML. Later ticks do nothing.
The node takes these inputs:
Input |
Default |
Description |
|---|---|---|
|
A single |
|
|
Path to a standard ros2_control YAML (the same file you would pass |
|
|
|
Optional overlay URDF that adds sensors or extra hardware params (see adding a sensor). Read from the importer’s |
|
|
ROS 2 namespace for the Controller Manager, its controllers, and |
|
|
Latch the synthesized URDF on |
|
|
Set |
The Controller Manager runs inside Isaac Sim, not as a separate ros2_control_node, so ps shows no such process and ros2 control commands talk to it directly. Its read, update, and write cycle runs from the physics step at the YAML’s update_rate, capped at the physics rate.
You do not write a URDF. The extension synthesizes one from USD on each Play and infers every joint’s command interface from its drive:
Joint drive in USD |
Command interface |
|---|---|
Stiffness above zero |
position |
Damping only (no stiffness) |
velocity |
Drive with neither gain |
effort |
No drive |
state only (reported, not commandable) |
Driven joints export position, velocity, and effort, so any standard controller works and you can switch modes at runtime. A joint with no drive shows up in /joint_states but cannot be commanded.
To configure the Controller Manager from a script instead of an Action Graph, call Ros2ControlManager.setup() and
Ros2ControlManager.teardown(). When using the default use_sim_time=True, provide /clock with
ROS2PublishClock or another ROS clock source. See the standalone example under
standalone_examples/api/isaacsim.ros2.control/.
Use your own robot#
The walkthrough uses a UR10, but the extension works with any articulation that meets the requirements below (verified with the UR10). Point targetPrim at your robot, give it a controller YAML, and the rest is the same as the walkthrough.
Your USD needs:
One
ArticulationRootAPIprim at or undertargetPrim.Revolute or prismatic joints for everything you want to control. Fixed and other joint types are skipped and get no
ros2_controlinterface.A drive on each joint you want to command. The drive’s gains pick the command interface, per the table above.
Write a standard ros2_control YAML for your controllers. You do not add a <hardware> block; the extension injects its own hardware interface. After Play, confirm the interfaces are present:
ros2 control list_hardware_interfaces
If the Controller Manager fails to start with ControllerManager init failed, the synthesized URDF or the YAML is the usual cause. Check that the joint names in your YAML match the robot.
Drive the arm without MoveIt#
Once both controllers are active you can command the arm directly. This is the fastest way to confirm the ros2_control pipeline independently of any MoveIt configuration. Publish a trajectory straight to the controller:
ros2 topic pub --once /scaled_joint_trajectory_controller/joint_trajectory \
trajectory_msgs/msg/JointTrajectory \
'{joint_names: ["shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint",
"wrist_1_joint", "wrist_2_joint", "wrist_3_joint"],
points: [{positions: [0.5, -1.2, 1.2, -1.5, -1.5, 0.0],
time_from_start: {sec: 3}}]}'
The UR10 moves to the commanded pose within a few seconds. The controller also serves a FollowJointTrajectory action on /scaled_joint_trajectory_controller/follow_joint_trajectory for goal-based clients such as MoveIt. If the arm moves here but not from MoveIt, the problem is in the MoveIt configuration, not the simulation side.
Add a force-torque or IMU sensor#
The synthesized URDF carries joints but no sensors. To publish force-torque or IMU data through a ros2_control broadcaster, supply a small overlay URDF that adds a <sensor> block and point the node’s urdfPath input at it. The URDF importer fills in urdfPath from isaac:sourceUrdf at import time, so you often do not set it by hand. The overlay only contributes <sensor> blocks and extra hardware <param> entries; the kinematic tree stays auto-generated.
Force-torque sensor. Bind the sensor to an articulation link, and add a matching force_torque_sensor_broadcaster to the controller YAML:
<robot name="arm">
<ros2_control name="arm">
<sensor name="ft_sensor">
<state_interface name="force.x"/>
<state_interface name="force.y"/>
<state_interface name="force.z"/>
<state_interface name="torque.x"/>
<state_interface name="torque.y"/>
<state_interface name="torque.z"/>
<param name="link">link1</param>
</sensor>
</ros2_control>
</robot>
controller_manager:
ros__parameters:
update_rate: 60
ft_broadcaster:
type: force_torque_sensor_broadcaster/ForceTorqueSensorBroadcaster
ft_broadcaster:
ros__parameters:
sensor_name: ft_sensor
frame_id: link1
The <param name="link"> must name a link in the articulation; an unknown link fails setup. The wrench is read from that link’s incoming joint force.
IMU sensor. Author an IsaacImuSensor prim under the articulation, then declare the ten IMU interfaces. The sensor is resolved by name to the matching IsaacImuSensor prim, so no prim_path param is needed:
<robot name="arm">
<ros2_control name="arm">
<sensor name="imu_sensor">
<state_interface name="orientation.x"/>
<state_interface name="orientation.y"/>
<state_interface name="orientation.z"/>
<state_interface name="orientation.w"/>
<state_interface name="angular_velocity.x"/>
<state_interface name="angular_velocity.y"/>
<state_interface name="angular_velocity.z"/>
<state_interface name="linear_acceleration.x"/>
<state_interface name="linear_acceleration.y"/>
<state_interface name="linear_acceleration.z"/>
</sensor>
</ros2_control>
</robot>
controller_manager:
ros__parameters:
update_rate: 60
imu_broadcaster:
type: imu_sensor_broadcaster/IMUSensorBroadcaster
imu_broadcaster:
ros__parameters:
sensor_name: imu_sensor
frame_id: link1
Note
IMU linear acceleration is reported in meters per second squared, following the REP-145 convention (a stationary sensor reads about 9.81 from gravity). Set the stage to meters; on a default centimeter stage the acceleration reads roughly 100 times too large.
Activate a broadcaster like any other controller, then read its topic:
ros2 control load_controller --set-state active ft_broadcaster
ros2 topic echo /ft_broadcaster/wrench
Mimic joints and grippers#
PhysX mimic joints, common in parallel grippers, come through automatically. The follower joint is reported in /joint_states and tracks its leader, but it is not separately commandable; command the leader and the follower follows.
Run multiple robots#
Each ROS2ControlManager node hosts one Controller Manager for one articulation root; pointing targetPrim at a scene with more than one root is rejected. To run several robots, add one node per robot and give each a distinct namespace. The namespace scopes that robot’s Controller Manager, its controllers, and its latched /robot_description. Leave each robot’s YAML flat; the node applies the namespace.
ros2 control list_controllers --controller-manager /arm_a/controller_manager
ros2 control list_controllers --controller-manager /arm_b/controller_manager
State and command topics resolve under each namespace (for example /arm_a/joint_states and /arm_b/joint_states), so the robots stay isolated.
Troubleshooting#
If update_rate in ur10_controllers.yaml is greater than the physics step rate, the Controller Manager clamps it to the physics rate and logs a one-time warning; the controllers keep running at the clamped rate. To run faster, raise the physics step rate so that update_rate is at most the physics step rate.
Pressing Stop tears down the in-process CM, so ros2 control list_controllers returns an error or empty list while the timeline is stopped. Pressing Play recreates the CM from scratch; re-run the launch from Terminal 2 to bring the controllers back. This is expected across Play and Stop cycles, not a crash.
If a setup error appears once and then nothing happens on later ticks, the node has latched the failure for this run. Fix the YAML or the USD, then press Stop and Play to retry.
If ros2 launch isaac_ros2_control_demo ur10_in_process.launch.py fails with package or controller plugin errors, confirm the apt packages from the prerequisites are installed:
sudo apt install ros-$ROS_DISTRO-ros2-controllers ros-$ROS_DISTRO-ur-moveit-config
If your RViz window shows a black screen where the robot should be, update your mesa driver:
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository ppa:kisak/kisak-mesa
sudo apt install -y mesa-utils
sudo apt -y upgrade
Summary#
You drove a UR10 in NVIDIA Isaac Sim with MoveIt 2 through a ros2_control Controller Manager hosted inside the simulation, commanded the arm directly, and saw how the extension maps a robot’s USD drives to ros2_control interfaces so you can use it with your own robots and sensors.
Next Steps#
Continue on to the next tutorial in our ROS 2 Tutorials series, ROS 2 Generic Server and Client, to create generic ROS 2 service servers and clients.
Further Learning#
Compare this with the topic-based approach in MoveIt 2, which drives MoveIt 2 over the ROS 2 bridge instead of an in-process Controller Manager.
Learn more about ros2_control.
Learn more about MoveIt 2.
Browse the upstream ros2_controllers plugin set.