Choosing a backend
PlannedMotionMixin gives an arm driver one planned-motion layer. The kinematics_backend attribute names the default planner behind it. The SDK has one built in:
"pinocchio"plans in-process on the robot description and executes through the arm’s trajectory controller.
set_pose, grasp, scene_set, attach_payload and detach_payload (see Commands), plus ee_pose. It also publishes the same state/motion stream, and stop cancels the active motion. A request can name any registered backend in backend; without one it gets the default.
self.replace_planned_motion(...). When a planned motion is running, it is cancelled and your work runs once that motion has ended, so the two never reach the arm controller at the same time. When it returns False, nothing was running and you send the motion yourself.
The in-process backend has two limits worth knowing:
- It has no sampling planner: a PTP path blocked by an obstacle is refused rather than routed around it.
- An
allow_touchobject is left out of the collision model, rather than allowed to touch only the gripper links.
backend_unavailable, and the detail starts with pinocchio:.
Bring your own backend
A driver that has another planner, such as MoveIt running beside it, can plug it in. Implement theMotionBackend protocol in your driver package, register it under a name, and select that name:
kinematics_backend in configure() from your driver’s config; it is read on every request. A name nobody registered fails loudly and lists the registered names.
Every backend that has been used holds the same task scene and payload. A scene_set, attach_payload or detach_payload reaches all of them or none, and a backend used for the first time is given the current scene before its first goal.
A backend that can’t plan right now refuses with backend_unavailable, and the detail names the backend. state/motion reports the default backend in kinematics_backend, whether it can accept goals in backend_available, the backend answering the current motion in backend, and the planner it used in planner.
See Integrating MoveIt for a complete example.
Commands
Every argument that is an option has a default, so a request carries only what it needs.set_pose
Moves the TCP to a pose, or by one.
twin.set_pose(...), twin.move(...) and, on an arm, twin.pose.set(...) send this command. They also take a CartesianPose, such as twin.pose.get(), and a (position, rotation) pair of arrays.
A relative target is measured from where the tool is when the command arrives, and a newer motion replaces a running one. Two relative moves sent back to back therefore don’t add up: the second starts wherever the first had got to. Use absolute targets for a sequence that has to land exactly.
ee_pose is set_pose with motion: "ptp" on the default backend.
grasp
Grasps the first reachable candidate of a ranked list. For each candidate in order, the backend plans:
- a PTP move to the pregrasp pose,
approach_distance_mback from the grasp pose; - a straight approach onto the grasp pose;
- a straight lift of
lift_distance_m, checked from the grasp pose but never executed.
state/motion reports the chosen candidate in last_result.selected_candidate. Every refused candidate raises its own planning_failed alert.
Scene and payload
A scene object’s
pose and a payload’s pose take every form set_pose accepts ([x, y, z, roll, pitch, yaw], [x, y, z, qx, qy, qz, qw], the [position, rotation] pair, or {position, orientation}), but no reference_frame: they are already in their fixed frame, the scene’s frame or the TCP.
Results
Commands carry no request id. Instead the driver numbers every motion it starts:state/motion reports the current one’s command, backend and motion_id, and the last_result or last_refusal that ends a motion names the same motion_id (plus the backend and, for a grasp, the selected candidate). After sending a command, watch for the first new motion_id reported with that command, then for the result that names it. When your command replaces a running motion, the replaced motion’s cancelled result names the older id.
A refusal of a command that never started has no motion_id. Every result also has a timestamp_ms that never repeats, so two identical refusals in a row stay distinct. A caller that needs its own verdict can also send the command as an MQTT v5 request: the driver replies accepted once the request has passed its gates and been handed to the backend, or rejected with the refusal line. accepted is not a plan: on the built-in pinocchio backend, planning runs inside that hand-off, so an accepted reply can already be followed by a planning_failed result. Wait for the result on state/motion before treating the motion as underway.
Cartesian requests
cyberwave.driver.control.cartesian_request checks an ee_pose or jog request before anything moves. The checks are fault latch, control mode, hardware verification, geometry, collision model and measured state. A refused request returns a reason code your caller can act on.
Local planning
LocalCartesianPlanner plans a straight-line (LIN) or joint-space (PTP) path with the arm’s own IK. It refuses the whole path if any sample has no IK solution, collides, or would flip the IK branch. It then smooths and times the trajectory within your velocity and acceleration limits. It is the planner of the pinocchio backend and of the Cartesian jogs.
Jogs (ee_move, ee_rotate_*) solve from the arm’s current posture only. A target that posture can’t reach is refused rather than answered with a different arm configuration.
Where planning runs
In-process planning runs on the driver’s event loop, inside the command handler. While a plan or a grasp search runs, the driver doesn’t take other commands, includingstop, and doesn’t publish status. Typical costs on a desktop CPU with a meshed arm:
- a successful LIN or PTP plan takes a few milliseconds;
- one reachable grasp candidate takes about 0.3 s;
- a refused grasp candidate takes about 0.07 s, so a 16-candidate search that finds nothing takes about 1 s.
scipy is imported when the planner is built, not on the first plan, and scene changes reuse the loaded collision meshes.
Collision-aware kinematics
CollisionAwareKinematics extends the base solver with the following:
- self-collision checking driven by your SRDF;
- an optional floor slab;
- obstacle primitives;
- an attached payload carried on the tool frame;
- multi-seed IK that stays on one branch along a path.
Motion limits and description checks
resolve_motion_limitsturns a single joint-velocity setting in rad/s into per-joint planner limits. It accounts for the URDF limits and your controller’s profile.evaluate_hardware_compatibilityrefuses planned motion when the firmware and the robot description don’t match.
Command arbiter
MotionArbiterCore is the single writer for an arm driven by a position stream. It is wrapped as a ROS node by cyberwave.driver.ros2.arbiter_node. Its rules:
- teleop preempts a planned trajectory;
- a trajectory owns the arm while its controller goal is active;
- faults latch until reset;
- stale commands are dropped.
TrajectoryExecutor sends and cancels FollowJointTrajectory goals.
World obstacles
AddWorldObstaclesMixin to give a twin add_obstacles and remove_obstacle commands. The obstacle list is saved in twin metadata and restored when the driver starts.
Joint command safety hooks
JointCommandBufferMixin has two hooks, both off by default:
- set
STOP_MOTION_ON_MQTT_OUTAGE = Trueto stop motion when the MQTT connection drops; - override
_joint_target_refusalto refuse a joint target, for example one that would collide.
Motion status
MotionStatusStream publishes the state/motion stream. Every planned-motion command reports its result there with a fixed set of refusal codes.
Test it on the robot
Before you ship, runcyberwave-stress-test --uuid <twin-uuid> against the twin. It reads which of these commands your driver exposes, runs the matching sections on the real robot, and judges each from measured feedback. See Stress-testing your driver.