New Controller
This guide explains how to create a new robot controller for motion planning with CuRobo.
Part 1: Create CuRobo Configuration Files
Our controller uses a separate CuRobo config file for each arm. You need to:
- Prepare URDF files for each arm - Extract single-arm URDF from your robot's full URDF
- Create CuRobo config files - Follow the official CuRobo tutorial
Step 1.1: Prepare URDF Files
For dual-arm robots, create separate URDF files for left and right arms. Save them in:
workflows/simbox/curobo/src/curobo/content/assets/robot/
├── your_robot_left_arm.urdf
├── your_robot_right_arm.urdf
└── meshes/Hint
Each arm URDF should be a standalone file containing only that arm's links and joints. Make sure mesh paths in the URDF are correct relative to the assets folder.
Step 1.2: Create CuRobo Config Files
Follow the official CuRobo tutorial: Configuring a New Robot
Key steps:
- Convert URDF to USD using Isaac Sim
- Use Lula Robot Description Editor to generate collision spheres
- Configure self-collision parameters
Save config files in:
workflows/simbox/curobo/src/curobo/content/configs/robot/
├── your_robot_left_arm.yml
└── your_robot_right_arm.ymlWarning
Isaac Sim's Lula Robot Description Editor is only available in Isaac Sim 4.0.0. Later versions have removed this feature. If you're using a newer version, you may need to manually create the collision sphere configuration or use Isaac Sim 4.0.0 for this step.
Example CuRobo Config Structure
# your_robot_left_arm.yml
robot_cfg:
kinematics:
usd_path: "robot/your_robot_left_arm.usd"
usd_robot_root: "/robot"
urdf_path: "robot/your_robot_left_arm.urdf"
asset_root_path: "robot"
base_link: "base_link"
ee_link: "link6" # End-effector link (must match fl_ee_path in robot.yaml)
collision_link_names:
- link1
- link2
# ...
collision_spheres:
link1:
- center: [0.0, 0.0, 0.0]
radius: 0.05
# ...
self_collision_ignore:
link1: ["link2", "link3"]
# ...
cspace:
joint_names: ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"]
retract_config: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
null_space_weight: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
cspace_distance_weight: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
max_jerk: 500.0
max_acceleration: 15.0Part 2: Update Robot YAML Configuration
In your new_robot.yaml (see New Robot), fill in the robot_file field:
For Dual-Arm Robots
# CuRobo kinematics config files (one per arm)
robot_file:
- workflows/simbox/curobo/src/curobo/content/configs/robot/your_robot_left_arm.yml
- workflows/simbox/curobo/src/curobo/content/configs/robot/your_robot_right_arm.ymlFor Single-Arm Robots
For single-arm robots, we conventionally name it as the "left" arm:
# CuRobo kinematics config file
robot_file:
- workflows/simbox/curobo/src/curobo/content/configs/robot/your_robot_left_arm.ymlPart 3: Create Controller Python Class
Create a new controller file in workflows/simbox/core/controllers/. Here's a new controller template:
"""NewRobot controller implementation."""
import numpy as np
from core.controllers.base_controller import register_controller
from core.controllers.template_controller import TemplateController
@register_controller
class NewRobotController(TemplateController):
"""Controller for NewRobot."""
def _get_default_ignore_substring(self):
"""Return default collision ignore substrings."""
return ["material", "table", "floor", "scene"]
def _configure_joint_indices(self, robot_file: str) -> None:
"""
Configure joint names and indices for motion planning.
This method maps between CuRobo's joint names and the robot USD's joint names.
"""
# Raw joint names from CuRobo config (must match cspace.joint_names in yaml)
self.raw_js_names = ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"]
if "left" in robot_file:
# Command joint names in robot USD (with prefix)
self.cmd_js_names = ["fl_joint1", "fl_joint2", "fl_joint3",
"fl_joint4", "fl_joint5", "fl_joint6"]
# Joint indices from robot config
self.arm_indices = np.array(self.robot.cfg["left_joint_indices"])
self.gripper_indices = np.array(self.robot.cfg["left_gripper_indices"])
self.reference_prim_path = self.task.robots[self.name].fl_base_path
self.lr_name = "left"
self._gripper_state = 1.0 if self.robot.left_gripper_state == 1.0 else -1.0
elif "right" in robot_file:
# Command joint names in robot USD (with prefix)
self.cmd_js_names = ["fr_joint1", "fr_joint2", "fr_joint3",
"fr_joint4", "fr_joint5", "fr_joint6"]
self.arm_indices = np.array(self.robot.cfg["right_joint_indices"])
self.gripper_indices = np.array(self.robot.cfg["right_gripper_indices"])
self.reference_prim_path = self.task.robots[self.name].fr_base_path
self.lr_name = "right"
self._gripper_state = 1.0 if self.robot.right_gripper_state == 1.0 else -1.0
else:
raise ValueError("robot_file must contain 'left' or 'right'")
# Gripper joint position for open/close
# Adjust based on your gripper's joint values
self._gripper_joint_position = np.array([0.044]) # Example: 44mm for open
def get_gripper_action(self):
"""
Map gripper state to joint positions.
Returns:
np.ndarray: Joint positions for gripper.
State mapping:
- _gripper_state = 1.0 -> Gripper OPEN
- _gripper_state = -1.0 -> Gripper CLOSED
"""
# When _gripper_state is 1.0, gripper opens (joint value = positive)
# When _gripper_state is -1.0, gripper closes (joint value = 0 or negative)
return np.clip(self._gripper_state * self._gripper_joint_position, 0.0, 0.1)_configure_joint_indices(self, robot_file: str)
Configure joint names and indices for motion planning. This method maps between CuRobo's joint names and the robot USD's joint names.
Parameters:
- robot_file (str): Path to the CuRobo config file, used to determine left/right arm.
Key Variables:
- raw_js_names (list): Joint names in CuRobo config (from
cspace.joint_namesin yaml). - cmd_js_names (list): Joint names in robot USD (must match actual USD joint names).
- arm_indices (np.ndarray): Joint indices for arm in articulation.
- gripper_indices (np.ndarray): Joint indices for gripper in articulation.
Warning
The order of raw_js_names and cmd_js_names must correspond! The i-th element in raw_js_names maps to the i-th element in cmd_js_names.
get_gripper_action(self)
Convert _gripper_state to actual gripper joint positions.
Returns:
- np.ndarray: Joint positions for gripper.
State Mapping:
- _gripper_state = 1.0: Gripper OPEN (joint value that fingers open)
- _gripper_state = -1.0: Gripper CLOSED (joint value that fingers close)
Code Example:
# Parallel gripper (single joint)
def get_gripper_action(self):
return np.clip(self._gripper_state * self._gripper_joint_position, 0.0, 0.1)
# Two-finger gripper (two joints, e.g., Franka Panda)
def get_gripper_action(self):
finger1 = np.clip(self._gripper_state * 0.04, 0.0, 0.04)
finger2 = np.clip(-self._gripper_state * 0.04, -0.04, 0.0)
return np.array([finger1, finger2])Register the Controller
Add your controller to workflows/simbox/core/controllers/__init__.py:
from core.controllers.newrobot_controller import NewRobotController
__all__ = [
# ... existing controllers
"NewRobotController",
]Checklist
- [ ] Prepare single-arm URDF files for each arm
- [ ] Create CuRobo config YAML files using Isaac Sim 4.0.0 (Lula)
- [ ] Update
robot_filein robot YAML configuration - [ ] Create controller Python class
- [ ] Implement
_get_default_ignore_substring() - [ ] Implement
_configure_joint_indices()with correct joint name mapping - [ ] Implement
get_gripper_action()with correct open/close mapping - [ ] Register controller in
__init__.py - [ ] Test with task configuration