Skip to main content
The Python SDK’s planned-motion layer has one built-in planner: in-process pinocchio. If your robot already has a MoveIt configuration, you can plan with MoveIt instead. Your driver hosts a small MoveIt backend and registers it with the SDK. Commands, refusal codes and the state/motion stream stay the same.

Why MoveIt isn’t in the SDK

The SDK is framework-neutral. MoveIt and its message package, moveit_msgs, exist only in ROS images, so code that imports them belongs to the driver that ships those images. The SDK defines the contract (MotionBackend) and a registry. Your driver supplies the implementation.

Run MoveIt as a sidecar

Run move_group in its own container beside your driver, the way the Kinova and Piper drivers do:
  • a moveit compose service on the cyberwaveos/ros2-moveit image, in the twin’s ROS namespace;
  • the plant (your driver or its vendor node) publishes robot_description, the TF tree, joint states and the MoveIt config package for this arm;
  • trajectory bridges expose the arm’s FollowJointTrajectory action, so MoveIt executes through the same controller as everything else.
Your driver image only needs the message definitions, for example ros-$ROS_DISTRO-moveit-msgs. Gate the moveit service’s health on an advertised action, such as sequence_move_group, not on the node name: the node registers before the robot model has loaded.

Write a MotionBackend

Put the backend in your driver package, next to the BaseROS2Driver node that owns its ROS clients. It needs:
  • name and planning_frame;
  • available() and unavailable_refusal(): cheap checks that the action servers and services are ready, and the reason when they aren’t;
  • validate_goal(goal) and plan_and_execute(goal, on_update, on_done): rclpy action clients for sequence_move_group (Pilz PTP and LIN) and move_action (OMPL). A goal is a MotionGoal (one pose, PTP or LIN) or a GraspGoal (ranked grasp candidates). Both arrive in your planning frame. A backend without a grasp search refuses a GraspGoal in validate_goal;
  • apply_scene(request, diff), attach(request) and detach(object_id): the apply_planning_scene and get_planning_scene services, sent as diffs;
  • cancel(): cancel the active goal, fire and forget.
Import moveit_msgs lazily, inside functions, or behind a guard. The driver then still starts without it (in unit tests, manifest export, or an image without MoveIt) and reports why planning is unavailable instead of crashing.

Register it and switch from config

Extend the registry with _motion_backend_factories and assign kinematics_backend in configure():
Keep "pinocchio" registered by extending super()’s dictionary rather than replacing it. If MoveIt plans in a frame other than the arm’s base frame, also override _motion_planning_frame() so the command catalog describes the frame you plan in.

Status and refusals

The SDK owns the contract, so a backend only reports facts:
  • state/motion carries kinematics_backend (the driver’s default backend), backend_available, backend (the one answering the current motion), and planner, the planner name your backend reports (for example pilz_ptp, pilz_lin or ompl);
  • when available() is false, every planned-motion command is refused with backend_unavailable. Build the refusal with backend_unavailable(name, detail) so the detail names the backend;
  • a failed plan is planning_failed, a controller that didn’t follow is execution_failed, and a stopped goal is cancelled;
  • guards, their order, scene revisions and the attached payload are handled by the SDK before your backend is called. A backend first used after a scene_set is given that scene, and any carried payload, before its first goal.
Your registered name is the default when kinematics_backend names it. A request can always name another registered backend in backend, for example "pinocchio" to plan in-process while MoveIt is the default.

Test with fakes

Every decision in a backend can run without ROS. Inject the MoveIt client into the backend, and test against a fake that records goals and answers with chosen error codes. At the driver level, register a fake MotionBackend through _motion_backend_factories, then drive the same command sequence with kinematics_backend = "pinocchio" and with your backend’s name. The two state/motion payloads should match, apart from the backend and planner names and the timing.

Reference implementations

  • The Piper driver’s MoveIt backend, cw_piper/motion/moveit/ in cyberwave-edge-nodes/piper/public: configuration, the planner chain (Pilz PTP, then OMPL on failures it can route around), the grasp as Pilz sequences, MoveItErrorCodes classification and the rclpy client, with its tests.
  • The Kinova Gen3 driver’s bridge, cyberwave_kinova_bringup/scripts/cyberwave_bridge.py: a guarded moveit_msgs import and a MoveGroupSequence client for Cartesian waypoint missions.