Skip to main content
An arm driver doesn’t have to build its own planning and safety layer. The Python SDK ships the pieces every arm needs, so a driver supplies configuration and the few hardware-specific hooks. Install the arm extras for kinematics, planning and collision checks:

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.
Whichever backend answers, the driver gets the same commands, arguments and refusal codes: 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.
If your driver also sends motion of its own, such as a Cartesian jog, pass that work to 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_touch object is left out of the collision model, rather than allowed to touch only the gripper links.
When it can’t plan at all (no arm controller, or no limits to time a trajectory with), it refuses with 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 the MotionBackend protocol in your driver package, register it under a name, and select that name:
Assign 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.
A pose always carries a rotation: there is no position-only form, and no degrees. From Python, 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:
  1. a PTP move to the pregrasp pose, approach_distance_m back from the grasp pose;
  2. a straight approach onto the grasp pose;
  3. a straight lift of lift_distance_m, checked from the grasp pose but never executed.
The first candidate whose three phases all plan is executed up to the grasp pose. The gripper is left for you to close, and 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, including stop, 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.
Edge computers are several times slower. Keep grasp searches small, and treat these costs as how long the arm can’t take another command. 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_limits turns 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_compatibility refuses 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

Add WorldObstaclesMixin 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 = True to stop motion when the MQTT connection drops;
  • override _joint_target_refusal to 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, run cyberwave-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.