Set up URDF for ros2_control
This document explains how to prepare your robot's URDF so that it can be driven through ros2_control on Isaac Sim, and what isaac_ros2_utils does with each part of the description behind the scenes.
Introduction to ros2_control
ros2_control offers developers a common API that allows your software to
switch between many different robot types (and simulators) by simply changing
a plugin in the URDF. The controller side (e.g. diff_drive_controller,
joint_trajectory_controller) stays exactly the same; only the hardware
interface that talks to the actual robot or simulator is replaced.
To use ros2_control with Isaac Sim, this repository uses
topic_based_ros2_control.
This hardware interface exchanges sensor_msgs/JointState messages with the
simulator over two ROS 2 topics:
- It subscribes to a joint states topic. Isaac Sim publishes the actual position / velocity / effort of every joint there.
- It publishes to a joint commands topic. Isaac Sim subscribes to it and forwards the commands to the joint drives.
The topic names are declared as hardware parameters:
<ros2_control name="diffbot" type="system">
<hardware>
<plugin>topic_based_ros2_control/TopicBasedSystem</plugin>
<param name="joint_commands_topic">/diffbot/joint_command</param>
<param name="joint_states_topic">/diffbot/joint_states</param>
<param name="sum_wrapped_joint_states">true</param>
</hardware>
...
</ros2_control>
How isaac_ros2_utils handles this: when the robot is spawned,
robot_controller.py reads the joint_commands_topic and
joint_states_topic parameters from the URDF and builds an OmniGraph
(a node network evaluated on every physics step) inside Isaac Sim:
OnPhysicsStep ─┬─> ArticulationState ──> ROS2Publisher ──> joint_states_topic
└─> ROS2Subscriber(joint_commands_topic) ──> ArticulationController
So from the ROS 2 side, Isaac Sim looks like a piece of hardware that reports
its joint states and accepts joint commands — which is exactly what
TopicBasedSystem expects.
Available hardware parameters:
| Parameter | Meaning |
|---|---|
joint_commands_topic |
Topic that Isaac Sim receives joint commands on |
joint_states_topic |
Topic that Isaac Sim publishes joint states on |
sum_wrapped_joint_states |
Set true for robots with continuous joints (e.g. wheels). Isaac Sim reports wrapped angles in the range of ±2π; with this option the hardware interface accumulates them into a continuous angle, which controllers like diff_drive_controller require for odometry. |
trigger_joint_command_threshold |
Publish a command message only when the command changes by more than this value (optional). |
Note: the joint state topic is tied to the topic names in the URDF, not to the robot name. When operating multiple robots, give each robot its own topic names (e.g. prefix them with the robot name).
Set up Joint Information
Inside the ros2_control tag, list every joint that ros2_control should
manage, with one command_interface (either position or velocity) and
three state_interfaces (position, velocity and effort):
<ros2_control name="diffbot" type="system">
<hardware>...</hardware>
<joint name="left_wheel_joint">
<command_interface name="velocity">
<param name="min">-1</param>
<param name="max"> 1</param>
</command_interface>
<state_interface name="position"/>
<state_interface name="velocity"/>
<state_interface name="effort"/>
</joint>
</ros2_control>
You can also give a joint an initial position:
<state_interface name="position">
<param name="initial_value">1.57</param>
</state_interface>
How isaac_ros2_utils handles this: for each joint listed here,
robot_controller.py finds the corresponding joint in the imported USD stage
and configures its drive:
- Joints with a
positioncommand interface get a position drive. If the URDF does not specify a stiffness (see below), a large default stiffness (1e8) is set so the joint tracks position commands rigidly. - Joints with a
velocitycommand interface get a velocity drive. If the URDF does not specify a damping, a default damping (15000) is set. initial_valueis written to the drive target at spawn time, so the robot starts in the given pose.
Set up Joint Stiffness, Damping and Joint Friction
In Isaac Sim, a joint drive outputs the following force (this is how both
position control and velocity control are realized — with a high stiffness
the joint acts as a position servo, with stiffness=0 and a high damping
it acts as a velocity servo):
force = stiffness * (target_position - position) + damping * (target_velocity - velocity)
Stiffness, damping and joint friction cannot be expressed in standard URDF.
This package defines them in an isaac_drive_api tag placed inside the
normal joint tag (not the one inside ros2_control):
<robot>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel_link"/>
<axis xyz="0 0 1"/>
<isaac_drive_api stiffness="0" damping="30000" joint_friction="10000"/>
</joint>
</robot>
How isaac_ros2_utils handles this: spawn.py reads the tag after the URDF
import and writes the values to the joint prim in USD:
stiffness/damping→ the joint'sPhysicsDriveAPIattributesjoint_friction→ the joint'sPhysxJointAPIfriction attribute
Rough tuning guide:
- Velocity-controlled wheels:
stiffness="0", largedamping - Position-controlled arm joints: large
stiffness, moderatedamping - If you omit the tag, the defaults described in the previous section are applied depending on the command interface.
Set up Friction of Link
Unlike Gazebo, Isaac Sim ties friction coefficients to a physics material
rather than to a link. This package therefore extends the URDF material tag
(normally only used for color) with an isaac_rigid_body tag:
<robot>
<material name="white">
<color rgba="1.0 1.0 1.0 1.0"/>
<isaac_rigid_body static_friction="1.0" dynamic_friction="1.0" restitution="0.0"/>
</material>
</robot>
Assign the material to a link with the visual tag as usual (there is no need
to duplicate the material in the collision tag):
<robot>
<link name="body_link">
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="1.0"/>
<inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<box size="0.24 0.18 0.06" />
</geometry>
<material name="white" />
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0" />
<geometry>
<box size="0.24 0.178 0.06" />
</geometry>
</collision>
</link>
</robot>
How isaac_ros2_utils handles this: for every material with an
isaac_rigid_body tag, spawn.py creates a USD physics material
(/<robot>/Looks/material_<name>) with the given friction/restitution
values. Then, for every link whose visual uses that material, the physics
material is bound to the link's collision geometry, so the friction acts on
contacts of that link.
Collision approximation (convex decomposition)
Isaac Sim's GPU physics cannot collide raw triangle meshes directly; mesh collisions are approximated. By default this package imports collision meshes with the convex decomposition approximation (the mesh is split into multiple convex pieces).
You can tune the maximum number of convex pieces per link with a
convex_decomposition tag inside the collision tag:
<collision>
<geometry>
<mesh filename="package://my_robot/meshes/body.stl"/>
</geometry>
<convex_decomposition max_convex_hulls="16"/>
</collision>
How isaac_ros2_utils handles this: after the import, spawn.py finds the
collision geometry of each link and applies
PhysxConvexDecompositionCollisionAPI with the given
max_convex_hulls (default: 32). More hulls follow concave shapes more
accurately at the cost of performance.