Why moveit_kit — the full MoveIt 2 C++ API from Python via cppyy¶
moveit_kit lets you drive MoveIt 2 — the standard C++ motion-
planning framework for robot arms — from Python: the real C++ code owns the robot
model, the planning scene (FCL collision), the KDL kinematics solver and the OMPL
planner, while your Python composes the problem and reads the result. It does this
against the MoveIt that is already installed, with no code generation and no build
step, using MoveIt's own C++ API names (RobotModel, RobotState::setFromIK,
PlanningScene::checkCollision, a real OMPL PlannerManager).
The point isn't "MoveIt has no Python binding" — it does, moveit_py. The point is
that moveit_py is an explicit, hand-curated subset, and cppyy gives you the
whole C++ surface. For the API cheat sheet, see SKILL.md; for
the feasibility evidence, the plugin/parameter bring-up mechanics, benchmarks, and gaps,
see REPORT.md.
The thing moveit_py makes you do without¶
moveit_py (the official pybind11 binding) wraps MoveItPy / PlanningComponent and a
curated slice of core (RobotState, PlanningScene, AllowedCollisionMatrix, a few
construct_* constraint helpers). It is a good high-level API — and a fixed surface:
only the methods someone chose to bind, in the shapes they chose. When you need a C++
method that wasn't wrapped, or a callback slot the binding omitted, you are stuck writing
and compiling a C++ node.
Concretely, and verified in this spike (REPORT.md §4): MoveIt C++
RobotState::setFromIK has an overload that takes a validity callback
(GroupStateValidityCallbackFn) which the solver invokes for every IK candidate — this
is how you get collision-aware IK. moveit_py's binding is
set_from_ik(group_name, pose, timeout): no callback parameter. So moveit_py hands
you back whatever the solver finds, in collision or not, with no way to reject it from
Python.
With cppyy that overload is just there, because cppyy reads MoveIt's headers:
cb = moveit_kit.state_validity_callback(
lambda rs, group, values: not scene.isStateColliding(rs, group.getName()))
state.setFromIK(jmg, target_pose, 0.2, cb) # the C++ KDL solver calls your Python
In the spike the C++ solver called this Python collision check 70–122 times inside a
single setFromIK, rejecting in-collision candidates — a capability moveit_py's API
cannot express regardless of how you set it up.
Side by side: build a model, do FK + IK, C++ vs Python¶
On the left, the shape of a MoveIt C++ program (RobotModelLoader + KDL IK) and its
build system. On the right, the runnable file this repo ships,
scripts/moveit_kit_demos/d01_robot_state.py.
C++ — robot_state.cpp + CMakeLists.txt (+ ament package)¶
#include <moveit/robot_model_loader/robot_model_loader.h>
#include <moveit/robot_state/robot_state.h>
#include <rclcpp/rclcpp.hpp>
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("ik"); // needs robot_description +
// robot_description_kinematics
// params (launch file / YAML)
robot_model_loader::RobotModelLoader loader(node); // reads params, loads KDL plugin
auto model = loader.getModel();
moveit::core::RobotState state(model);
const auto* jmg = model->getJointModelGroup("panda_arm");
state.setToDefaultValues(jmg, "ready");
state.update();
Eigen::Isometry3d target = state.getGlobalLinkTransform("panda_link8");
state.setToRandomPositions(jmg);
bool ok = state.setFromIK(jmg, target, 0.1);
RCLCPP_INFO(node->get_logger(), "IK %s", ok ? "solved" : "failed");
}
find_package(moveit_core REQUIRED)
find_package(moveit_ros_planning REQUIRED)
find_package(rclcpp REQUIRED)
add_executable(ik robot_state.cpp)
ament_target_dependencies(ik moveit_core moveit_ros_planning rclcpp)
…and this does not run yet: you need the CMakeLists.txt, a package.xml, colcon
build, a launch file that puts robot_description + robot_description_kinematics on
the node's parameter server, and a rebuild on every edit.
Python — d01_robot_state.py (moveit_kit, shipped in this repo)¶
import rclcpp_kit
import moveit_kit
moveit = moveit_kit.bringup_moveit(with_kinematics=True) # parse + KDL plugin
cfg = moveit_kit.panda_config()
model = moveit_kit.build_robot_model(cfg.urdf, cfg.srdf) # from URDF+SRDF strings
jmg = model.getJointModelGroup("panda_arm")
state = moveit.core.RobotState(model)
state.setToDefaultValues(jmg, "ready"); state.update()
target = state.getGlobalLinkTransform("panda_link8") # FK
rclcpp = bringup_rclcpp(); rclcpp.ok() or rclcpp.init()
node = moveit_kit.make_node("ik")
moveit_kit.load_kinematics_solver(node, model, "panda_arm") # KDL via pluginlib
state.setToRandomPositions(jmg); state.update()
print("IK", "solved" if state.setFromIK(jmg, target, 0.1) else "failed") # IK
Run it directly: pixi run -e moveit demo-moveit-state. RobotState, setFromIK,
getGlobalLinkTransform are MoveIt's own C++ methods — the kit only assembles the
node parameters and loads the KDL plugin for you (the plugin/parameter bootstrap that
would otherwise be a launch file; see REPORT.md §2).
What we gain¶
- No compile step, no CMake, no launch file.
python x.pyis the workflow. The model comes from URDF+SRDF strings, and the kit boots MoveIt's plugin/parameter stack in-process (the bit that normally forces a launch file). - The full C++ surface, header-following. Anything in MoveIt's headers is reachable —
including the
setFromIKvalidity-callback overload moveit_py omits. cppyy reads the installed 2.12.4 headers, so the kit tracks whatever MoveIt is installed. - Python in the loop where it helps. A collision check, a custom constraint, a
heuristic can be a plain Python function the C++ solver calls — with a breakpoint and
printin it. (See REPORT.md §3 for what that costs; collision checking itself runs at moveit_py's own speed — ~129k checks/sec.)
What the C++ version buys that this one doesn't. A compiled binary pays no JIT at
startup and gets static type-checking; the full PlanningPipeline (with time-
parameterization and the request/response adapters) is available in C++ but its header
crashes cppyy's parser, so the kit uses the planner plugin directly (geometric trajectory,
no timing — REPORT.md §5).
The showcase: plan the Panda with MoveIt's real OMPL pipeline, from Python¶
scripts/moveit_kit_demos/d02_plan_pose_goal.py plans the Panda arm to a Cartesian pose
goal using MoveIt's real OMPL PlannerManager plugin — the same .so move_group
loads — configured from ompl_planning.yaml, then publishes the result as a
moveit_msgs/DisplayTrajectory on /display_planned_path so RViz renders it:
moveit_kit.bringup_moveit(with_kinematics=True, with_planning=True)
node = moveit_kit.make_node("plan", moveit_kit.parameter_overrides(cfg.ompl, "ompl"))
moveit_kit.load_kinematics_solver(node, model, "panda_arm")
planner = moveit_kit.load_planner(node, model) # OMPL, via pluginlib
result = moveit_kit.plan_pose_goal(planner, scene, "panda_arm", "panda_link8", target)
pub.publish(moveit_kit.display_trajectory(result, scene)) # rviz-compatible
The planner selects geometric::RRTConnect from the panda config and solves in a few
milliseconds. pixi run -e moveit demo-moveit-plan. This is the full MoveIt planning
stack — RobotModel + PlanningScene/FCL + KDL kinematics + OMPL — driven from ~30 lines of
Python, no code generation.
Advantages of the cppyy approach¶
Grounded in the spike's measured numbers (see REPORT.md):
- The whole C++ API, not a curated subset. The
setFromIKvalidity-callback, directPlanningScene/ACM/world manipulation, the planner plugin — all reachable; moveit_py binds a slice. - No codegen, no build, header-following.
pixi install -e moveitandpython x.py; the kit tracks the installed MoveIt. - In-process plugin/parameter bootstrap. The kit boots MoveIt's pluginlib + node-
parameter stack from a Python-created node (REPORT.md §2) — the mechanic that normally
requires
move_group+ a launch file — and the next kit (ros2_control) reuses it. - No speed tax on the hot path. Collision checking runs at moveit_py's own throughput (~129k checks/sec); the full API is reachable at the subset's speed.
Limits¶
moveit_kit is a v0 spike focused on planning, not execution. It plans but does not
drive controllers (that is the ros2_control kit); the trajectory is geometric (no time
parameterization, because the full PlanningPipeline header crashes Cling and only the
planner plugin is used); pose-goal planning needs the kinematics solver loaded; the
config helper is panda-specific; and MoveItServo / CHOMP/STOMP/Pilz are reachable via the
same pluginlib mechanic but not surfaced. The full, honest list is in
REPORT.md §5.