This repository provides robot-agnostic ROS2 packages that interface the Closed-Chain Affordance(CCA) planner with robotic systems. The CCA planner offers an intuitive approach to planning joint trajectories for robot manipulation tasks that can be thought of as linear, rotational, or screw motions. Defining a task is as simple as specifying an axis, location, and pitch (if applicable). Additionally, it provides the capability to control the end-effector's orientation along the task path. The demonstration video of the underlying paper showcasing simulation and real-world tasks is available here. An interactive RViz plugin is also available as a way to interface with the planner. See the plugin demo videos for a glance at the framework's capabilities.
C++20ROS Humble
- CCA planner - Install from here.
moveit: For self-collision checkingmoveit_visual_tools: For visualization of joint movementbehaviortree_cpp: To utilize the CCA Behavior Tree action node
With ROS sourced, you may install the ROS dependencies with:
sudo apt install ros-${ROS_DISTRO}-moveit ros-${ROS_DISTRO}-moveit-visual-tools ros-${ROS_DISTRO}-behaviortree-cpp-
Clone the packages into your ROS2 workspace's
srcfolder, for example:mkdir -p ~/ws_cca_ros/src && cd ~/ws_cca_ros/src git clone git@github.com:UTNuclearRoboticsPublic/closed_chain_affordance_ros.git
-
Build and source the workspace:
cd ~/ws_cca_ros colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release source install/setup.bash
An additional cca_<robot> package containing robot-specific information is required to launch the planner for a particular robot. Packages are currently available for the following robots, with links provided below. Creating a package for a new robot is simple, quick, and largely automated, as discussed in the Implementing the Framework on a New Robot section.
There are four ways to interact with the CCA planner through ROS2:
- Programmatic Planning and Execution: Directly call the CCA planner from within a ROS2 node (e.g., the autogenerated
cca_<robot>_node.cpp). See the Code Tutorial section for details. - Behavior Tree Integration: Use the CCA planner as a node within a Behavior Tree. See
cca_ros_behaviorpackage for details. Xml usage is as follows:<!-- for a single request, req, a shared ptr to a planning request --> <CcaRosAction cca_planning_request="{req}"/> <!-- or for multiple requests, reqs, a shared ptr to a vector of planning requests --> <CcaRosAction cca_planning_requests="{reqs}"/>
- Action Server: Use the CCA action server to send planning requests and receive planned trajectories. See
cca_ros_actionpackage for details. - Rviz Plugin: A user-friendly Rviz plugin is also available and enables visual trajectory planning and execution by simply dragging interactive markers and specifying task types and goals. Launch instructions are provided in the Interactive Rviz Plugin Planning section.
-
Use the package creator script:
cd ~/ws_cca_ros/src/closed_chain_affordance_ros ./cca_robot_package_creator.sh
-
Configure the generated package:
-
Complete the
config/cca_<robot>_description.yamlfile, which contains information about the robot kinematic chain, planning group, relevant action servers, etc. -
Complete the
launch/cca_<robot>_settings.pymodule by providing information about how to generate robot description and robot description semantic parameters. Inline comments provide detailed guidance on filling out these files. -
For programmatic trajectory planning and execution, implement task (affordance) details in
cca_<robot>_node.cpp. Alternatively, you may use the Rviz plugin for interactive planning.
-
-
Build the new package:
cd ~/<ros_workspace_name> colcon build --packages-select cca_<robot> --cmake-args -DCMAKE_BUILD_TYPE=Release source install/setup.bash
- For real-robot execution, ensure a
follow_joint_trajectoryaction server is active,joint_statesare being published, and TF data is available.
To plan without a physical robot, simply providejoint_statesand TF data.
-
Launch trajectory validation and visualization server:
ros2 launch cca_<robot> cca_<robot>_val_and_viz.launch.py launch_rviz:=true
-
Run the planner for tasks defined in
cca_<robot>_node.cpp:ros2 launch cca_<robot> cca_<robot>.launch.py
-
Start the CCA ROS action server:
ros2 launch cca_<robot> cca_<robot>_action_server.launch.py
-
Launch Rviz with the interactive planning plugin:
ros2 launch cca_<robot> cca_<robot>_val_and_viz.launch.py launch_rviz:=true
Here, we provide a brief tutorial on how to use the CCA planner programmatically within a ROS2 node.
We use a request–response model to communicate with the planner.
A typical request is structured as follows:
cca_ros::PlanningRequest req;
req.task_description = // See examples below in the "Task Description" section
req.planning_group = // Name of the planning group for which to plan, for example "arm"
req.execute_trajectory = true; // Set to true to execute the trajectory on the robot (default: false)
req.time_step = // Time interval (in seconds) between consecutive trajectory points, for example req.time_step.robot = 0.1;These are rarely needed in typical usage:
req.planner_config = // Custom planner configuration (default settings are usually sufficient). See more info below.
req.start_state = // Specify a starting joint configuration (default: current robot state)One may also ask the planner to plan for a series of tasks in one go. This produces one long trajectory that accomplishes all tasks sequentially.
std::vector<cca_ros::PlanningRequest> reqs;
// Fill in each req as above
cca_ros::PlanningRequest req1;
reqs.push_back(req1);
cca_ros::PlanningRequest req2;
reqs.push_back(req2);
// and so on...Once the request(s) are defined, call the planner as follows:
// Create the planner node object
auto node = std::make_shared<CcaRos>("cca_ros");
// Spin the node so joint states can be read
std::jthread spinner_thread([node]() { rclcpp::spin(node); });
// Call the planner
cca_ros::PlanningResponse resp = node->plan(req); // For single request
// Or for multiple requests
// cca_ros::PlanningResponse resp = this->plan(reqs);
// Checking response
// Success is indicated by resp.result.success
bool planner_success = resp.result.success;
// For trajectory execution, one can also check planner motion status with
auto motion_status = response.status;
// See CCA ROS Doxygen documentation for details on status codes as well as other fields in the response,
// which may be of use to your application.Broadly, CCA provides four planning types:
- Affordance Planning: Plan tasks in terms of elementary motions—linear, rotational, or screw. Affordance describes the type of motion that an object offers as a task. For instance, a valve "affords" turning, which could be modeled as rotational motion.
- Cartesian Goal Planning: Plan tasks to reach a specified Cartesian goal pose.
- In-Place EE Orientation Control: Plan to adjust the end effector (EE) orientation about a specific axis without translational movement.
- Approach Planning: Given an affordance definition and a reference pose, plan to a pose along the affordance path that achieves a specified affordance goal. For example, move from an arbitrary current configuration to a pose that corresponds to 90 degrees along a rotational path defined by the affordance.
Below, we provide task examples for each of these categories as well as various affordance types. Note all vectors are defined in the reference frame specified in the config/cca_<robot>_description.yaml of the cca_<robot> package.
We want to move 0.8m along the z-axis
// Task instantiation
cc_affordance_planner::TaskDescription task_description(cc_affordance_planner::PlanningType::AFFORDANCE);
// Affordance info
task_description.affordance_info.type = affordance_util::ScrewType::TRANSLATION;
task_description.affordance_info.axis = Eigen::Vector3d(0, 0, 1);
task_description.affordance_info.location = Eigen::Vector3d::Zero();
// Goals
task_description.goal.affordance = 0.8;
// Optional: Constrain the EE to a particular desired orientation along the path
task_description.goal.ee_orientation = Eigen::Vector3d(M_PI/12, M_PI/8, M_PI/12); // as EE-frame rpyWe want to rotate about the z-axis by 270degrees
// Task instantiation
cc_affordance_planner::TaskDescription task_description(cc_affordance_planner::PlanningType::AFFORDANCE);
// Affordance info
task_description.affordance_info.type = affordance_util::ScrewType::ROTATION;
task_description.affordance_info.axis = Eigen::Vector3d(0, 0, 1);
task_description.affordance_info.location = Eigen::Vector3d(0.0, 0.0, 0.8);
// Goals
task_description.goal.affordance = 3.0 * M_PI / 2.0;We want to perform a screw motion about the negative y-axis by 270degrees at a 0.1m/rad pitch
// Task instantiation
cc_affordance_planner::TaskDescription task_description(cc_affordance_planner::PlanningType::AFFORDANCE);
// Affordance info
task_description.affordance_info.type = affordance_util::ScrewType::SCREW;
task_description.affordance_info.axis = Eigen::Vector3d(0, -1, 0);
task_description.affordance_info.location = Eigen::Vector3d(-0.4, 0, 0.5);
task_description.affordance_info.pitch = 0.1;
// Goals
task_description.goal.affordance = 3.0 * M_PI / 2.0;While keeping the EE position fixed, we want to change its orientation about the x-axis by 90degrees
// Task instatiation
cc_affordance_planner::TaskDescription task_description(cc_affordance_planner::PlanningType::EE_ORIENTATION_ONLY);
// Affordance info
task_description.affordance_info.axis = Eigen::Vector3d(1, 0, 0);
// Goal
task_description.goal.affordance = M_PI / 2.0;We want to plan to a desired cartesian goal
// Task instatiation
cc_affordance_planner::TaskDescription task_description(cc_affordance_planner::PlanningType::CARTESIAN_GOAL);
// Goal
task_description.goal.canonical_pose = Eigen::Matrix4d::Identity(); // as a 4x4 Homogeneous Transformation Matrix
task_description.goal.canonical_pose.block<3, 1>(0, 3) =
(Eigen::Vector3d() << 0.70932, 0.000336774, -0.017661).finished(); // we specify position, but leave orientation as identity for this example.We want to move from the current configuration to a pose along the affordance path, positioned 90 degrees from the specified reference (canonical) pose.
// Task instatiation
task_description = cc_affordance_planner::TaskDescription(cc_affordance_planner::PlanningType::APPROACH);
// Affordance info
task_description.affordance_info.type = affordance_util::ScrewType::ROTATION;
task_description.affordance_info.axis = Eigen::Vector3d(1, 0, 0);
task_description.affordance_info.location = Eigen::Vector3d::Zero();
// Goal
task_description.goal.affordance = M_PI / 2.0; // Set desired goal for the affordance
task_description.goal.canonical_pose = Eigen::Matrix4d::Identity();
task_description.goal.canonical_pose.block<3, 1>(0, 3) = Eigen::Vector3d(0.3, 0.3, 0.5);In the following examples, we specify the task in different frames that can be looked up in the TF tree.
We want to do a 90-degree rotation about the z axis of the EE frame
// Affordance info from -- Gets axis and location
task_description.affordance_info.from.method = affordance_util::PoseSpecificationMethod::FROM_FRAME_NAME;
task_description.affordance_info.from.frame_name = "ee_frame"; // Specify a valid frame name that appears in the TF tree
task_description.affordance_info.from.axis_in_final_pose = affordance_util::axis_to_vec(affordance_util::Axis::Z); // Choose z-axis of that frame
// Specify type
task_description.affordance_info.type = affordance_util::ScrewType::ROTATION;
// Goal
task_description.goal.affordance = M_PI / 2.0; // Set desired goal for the affordanceWe want to do a 90-degree rotation about the z axis of some frame B defined in relation to some frame A
// Affordance info from -- Gets axis and location
task_description.affordance_info.from.method = affordance_util::PoseSpecificationMethod::FROM_FRAME_NAME;
task_description.affordance_info.from.frame_name = "frame_a"; // Specify a valid frame name that appears in the TF tree
// Define the transform from frame_a to frame_b -- For simple example, just 10cm along the x-axis.
Eigen::Isometry3d T_a_to_b = Eigen::Isometry3d::Identity();
T_a_to_b.translation() = Eigen::Vector3d(0.1, 0.0, 0.0);
task_description.affordance_info.from.post_transform = T_a_to_b.matrix();
task_description.affordance_info.from.axis_in_final_pose = affordance_util::axis_to_vec(affordance_util::Axis::Z); // Choose z-axis of frame b
// Specify type
task_description.affordance_info.type = affordance_util::ScrewType::ROTATION;
// Goal
task_description.goal.affordance = M_PI / 2.0; // Set desired goal for the affordanceWe want to plan to a cartesian pose offset from some frame
// Task instatiation
cc_affordance_planner::TaskDescription task_description(cc_affordance_planner::PlanningType::CARTESIAN_GOAL);
task_description.canonical_pose_from.method = affordance_util::PoseSpecificationMethod::FROM_FRAME_NAME;
task_description.canonical_pose_from.frame_name = "frame_a"; // Specify a valid frame name that appears in the TF tree
// Define the transform from frame_a to frame_b --
Eigen::Isometry3d T_a_to_b = Eigen::Isometry3d::Identity();
T_a_to_b.linear() = Eigen::AngleAxisd(M_PI / 2.0, Eigen::Vector3d::UnitX()).toRotationMatrix(); // 90-deg rotation about the x-axis
T_a_to_b.translation() = Eigen::Vector3d(0.1, 0.0, 0.0); // 10cm along the x-axis
task_description.canonical_pose_from.post_transform = T_a_to_b.matrix(); Occasionally, you may need to consider the following settings for the task:
task_description.trajectory_density = 50; // Require the joint trajectory to have 50 points. By default this is 10And seldom, you may need to touch planner settings
cc_affordance_planner::PlannerConfig plannerConfig;
plannerConfig.ik_max_itr = 1000; // Default is 200. Raise this if the planner seems not to solve joint trajectories you think are feasible.
plannerConfig.accuracy = 1.0/100; // Accuracy of the planner, 1% for example. Default is 10%We leave the other settings to the advanced reader to persue through the CCA library documentation
Janak Panthi (aka Crasun Jans)