Robonix package for arm motion planning + execution on the Piper + Orbbec Dabai DCW grasp pipeline. Wraps the upstream MoveIt config (piper_with_gripper_moveit) plus the C++ moveit_control_node_yolo grasp executor. Owns the service/manipulation/* namespace.
Catalog name: robonix.service.piper_moveit.
Capability surface
Contract
Mode
Transport
Source / handler
robonix/service/manipulation/driver
rpc
gRPC
Driver(CMD_INIT, config_json) — lifecycle gate
robonix/service/manipulation/execute_grasp
rpc
gRPC/MCP
ExecuteGrasp(pose, gripper_width, timeout) → result
robonix/service/manipulation/reset
rpc
gRPC
Reset() → move arm to safe home pose
Extra contract TOMLs shipped under capabilities/service/manipulation/ (demo, demo_place) are handler helpers used during bring-up; they are not exposed in the deploy manifest.
Design intent (most-conservative path)
The C++ moveit_control_node_yolo is battle-tested upstream. The MoveIt MoveGroupInterface Python API is less stable than its C++ counterpart. Re-implementing the planning + execution logic in Python carries real risk for zero gain. So we vendor the C++ source verbatim and put a typed front door on it via ROS topic + status poll:
component
provenance
what we do
MoveIt config (URDF/SRDF/…)
upstream piper_with_gripper_moveit
vendor verbatim, colcon-build
moveit_control_node_yolo
upstream piper_moveit_control
vendor verbatim, colcon-build
Top-level launch
upstream piper_moveit.launch.py
fork (see below)
atlas Service wrapper
new (piper_moveit/main.py)
only Python we own
Signalling flow
Pilot LLM (or skill-pick-rbnx)
│
▼
MCP manipulation/execute_grasp (PoseStamped + width)
│
▼
┌─ publishes /graspnet/grasps (graspnet_msgs/GraspPose)
│
▼
C++ moveit_control_node_yolo (vendored)
│
├─ tf2 transform → arm/base_link
├─ MoveGroupInterface plan
└─ execute via FollowJointTrajectory action
│
▼
primitive-agilex-piper-arm-rbnx executes on real hardware
│
▼
piper_ctl publishes /arm/arm_status (PiperStatusMsg)
│
▼
◄─── execute_grasp polls busy → idle, returns success/elapsed
Launch fork (launch/piper_moveit_rbnx.launch.py)
Four deltas from upstream piper_moveit.launch.py:
Removed fake ros2_control_node — upstream’s launch spawns a simulated joint driver publishing /arm/joint_states. With primitive-agilex-piper-arm-rbnx already publishing real hardware joint states, running both = two publishers fighting on the topic = MoveIt plans against garbage.
Removed RViz — robonix-managed deploy is headless. Bring up RViz manually in another shell if needed:
move_group joint_states remap target switched from /arm/joint_states (which had no real publisher after we removed the fake controller) to /arm/joint_states_single (what primitive-agilex-piper-arm-rbnx actually publishes).
Added link6 → arm/link6 static TF bridge to glue the unprefixed TF tree (primitive-agilex-piper-description-rbnx publishes; primitive-agilex-piper-handeye-rbnx publishes link6 → camera_color_optical_frame into) onto the prefixed one (this launch publishes via frame_prefix="arm/" for MoveIt’s planning frame). Without the bridge, the C++ node’s tf_buffer_->transform(grasp_pose_in_camera_frame, "arm/base_link") call fails because the camera frame and arm/base_link are in different subtrees.
The rest (rsp.launch.py, static_virtual_joint_tfs.launch.py) is included verbatim via IncludeLaunchDescription, so future upstream tweaks don’t need a re-fork.
Why no spawn_controllers.launch.py?
Upstream uses spawn_controllers to start joint_trajectory_controller + gripper_controller ros2_control instances. Those drive the fake controller_manager we removed. The C++ node does not talk to controller_manager — it submits FollowJointTrajectory action goals directly to whoever advertises the action server. primitive-agilex-piper-arm-rbnx must advertise that action server for the real-hardware path to work.
Frame tree (post-boot)
Two parallel subtrees, joined at link6 ↔ arm/link6:
Both subtrees describe the same physical robot, just under different frame-name conventions. The bridge means tf_buffer_->transform(camera_color_optical_frame_pose, arm/base_link) walks: camera_* → link6 → arm/link6 → arm/base_link, and works.
Boot ordering
Requires primitive-agilex-piper-arm-rbnx (publishes /arm/arm_status; the sentinel waits for the first sample);
Requires primitive-agilex-piper-handeye-rbnx (static link6 → camera_color_optical_frame TF used inside execute_grasp);
Requires primitive-agilex-piper-description-rbnx (provides the unprefixed TF subtree);
Independent of service-object-detect-rbnx / service-grasp-pose-rbnx at boot time (no perception dep during Init).
on_init is heavy — sentinel timeout default 60 s because launch takes ~5–10 s to bring up move_group, and we need real piper joint feedback to flow before we declare ourselves ACTIVE.
execute_grasp semantics — two-phase wait
busy phase (≤ ⅓ × timeout_s): wait for /arm/arm_status to transition to non-zero (i.e. arm started moving). If never: either the C++ node did not pick up our publish, or the goal was already physically satisfied → fail conservatively.
idle phase (remaining budget): wait for return to 0 (idle).
arm_group_name: arm # MoveIt SRDF group
end_effector_link: arm/link6 # in prefixed tree
gripper_action_name: /gripper_controller/follow_joint_trajectory
sentinel_timeout_s: 60.0 # wait for /arm/arm_status
All four are optional (defaults match upstream). end_effector_link intentionally uses the prefixed arm/link6 — MoveIt’s planning frame is arm/world, so the EE link lives in the prefixed subtree.
service-piper-moveit-rbnx
Robonix package for arm motion planning + execution on the Piper + Orbbec Dabai DCW grasp pipeline. Wraps the upstream MoveIt config (
piper_with_gripper_moveit) plus the C++moveit_control_node_yolograsp executor. Owns theservice/manipulation/*namespace.Catalog name:
robonix.service.piper_moveit.Capability surface
robonix/service/manipulation/driverDriver(CMD_INIT, config_json)— lifecycle gaterobonix/service/manipulation/execute_graspExecuteGrasp(pose, gripper_width, timeout) → resultrobonix/service/manipulation/resetReset() → move arm to safe home poseExtra contract TOMLs shipped under
capabilities/service/manipulation/(demo,demo_place) are handler helpers used during bring-up; they are not exposed in the deploy manifest.Design intent (most-conservative path)
The C++
moveit_control_node_yolois battle-tested upstream. The MoveItMoveGroupInterfacePython API is less stable than its C++ counterpart. Re-implementing the planning + execution logic in Python carries real risk for zero gain. So we vendor the C++ source verbatim and put a typed front door on it via ROS topic + status poll:piper_with_gripper_moveitmoveit_control_node_yolopiper_moveit_controlpiper_moveit.launch.pypiper_moveit/main.py)Signalling flow
Launch fork (
launch/piper_moveit_rbnx.launch.py)Four deltas from upstream
piper_moveit.launch.py:ros2_control_node— upstream’s launch spawns a simulated joint driver publishing/arm/joint_states. Withprimitive-agilex-piper-arm-rbnxalready publishing real hardware joint states, running both = two publishers fighting on the topic = MoveIt plans against garbage.move_group joint_statesremap target switched from/arm/joint_states(which had no real publisher after we removed the fake controller) to/arm/joint_states_single(whatprimitive-agilex-piper-arm-rbnxactually publishes).link6 → arm/link6static TF bridge to glue the unprefixed TF tree (primitive-agilex-piper-description-rbnxpublishes;primitive-agilex-piper-handeye-rbnxpublisheslink6 → camera_color_optical_frameinto) onto the prefixed one (this launch publishes viaframe_prefix="arm/"for MoveIt’s planning frame). Without the bridge, the C++ node’stf_buffer_->transform(grasp_pose_in_camera_frame, "arm/base_link")call fails because the camera frame andarm/base_linkare in different subtrees.The rest (
rsp.launch.py,static_virtual_joint_tfs.launch.py) is included verbatim viaIncludeLaunchDescription, so future upstream tweaks don’t need a re-fork.Why no
spawn_controllers.launch.py?Upstream uses
spawn_controllersto startjoint_trajectory_controller+gripper_controllerros2_control instances. Those drive the fakecontroller_managerwe removed. The C++ node does not talk tocontroller_manager— it submitsFollowJointTrajectoryaction goals directly to whoever advertises the action server.primitive-agilex-piper-arm-rbnxmust advertise that action server for the real-hardware path to work.Frame tree (post-boot)
Two parallel subtrees, joined at
link6 ↔ arm/link6:Both subtrees describe the same physical robot, just under different frame-name conventions. The bridge means
tf_buffer_->transform(camera_color_optical_frame_pose, arm/base_link)walks:camera_* → link6 → arm/link6 → arm/base_link, and works.Boot ordering
primitive-agilex-piper-arm-rbnx(publishes/arm/arm_status; the sentinel waits for the first sample);primitive-agilex-piper-handeye-rbnx(staticlink6 → camera_color_optical_frameTF used insideexecute_grasp);primitive-agilex-piper-description-rbnx(provides the unprefixed TF subtree);service-object-detect-rbnx/service-grasp-pose-rbnxat boot time (no perception dep during Init).Driver-init lifecycle
on_initis heavy — sentinel timeout default 60 s because launch takes ~5–10 s to bring upmove_group, and we need real piper joint feedback to flow before we declare ourselves ACTIVE.execute_graspsemantics — two-phase waittimeout_s): wait for/arm/arm_statusto transition to non-zero (i.e. arm started moving). If never: either the C++ node did not pick up our publish, or the goal was already physically satisfied → fail conservatively.success=Trueiff both phases complete in budget.Layout
Config (passed via
Driver(CMD_INIT, config_json))All four are optional (defaults match upstream).
end_effector_linkintentionally uses the prefixedarm/link6— MoveIt’s planning frame isarm/world, so the EE link lives in the prefixed subtree.Build / run standalone
Verification
Failure modes
sentinel: no /arm/arm_status sample within 60sauto_can_setupfailedrbnx caps | grep piper_ctlto confirm; bring up CAN manually if neededexecute_grasp: timeout: arm_status never went busy/graspnet/grasps; or move_group rejected the goal silentlyros2 topic info /graspnet/graspsfor subs; checkmove_grouplogs/arm/joint_states_singleremap not taking effectjoint_states_topicinmove_group_configurationarm/base_link → camera_color_optical_framelink6 ↔ arm/link6not running, OR easy_handeye2 calib not stagedros2 run tf2_ros tf2_echo link6 arm/link6is identity; verify easy_handeye2 is ACTIVEFollowJointTrajectory action server not advertisedLicense
This package: MulanPSL-2.0. Vendored MoveIt config + grasp executor: see their respective LICENSE files.