Arm and gripper control, safety gating, and grasp execution: everything that turns a scored 6-DoF grasp candidate into motion a real cell may perform.
Application code talks to the RobotArm and Gripper Protocols in core/ and to
the vendor StrEnums. It never imports a vendor SDK, and it never has to know which arm is attached.
This is the lower middle of a strict downward dependency stack:
config -> geometry -> perception -> robot -> rl -> replay/KPI
Nothing in this package imports a layer above it. Two boundary rules hold across the whole repository and both are checked mechanically:
- No module under
src/imports a web framework (fastapi,uvicorn,starlette). api/is the one tree that may, aside from the console's own test modules, which cannot exercise an HTTP surface without a client for it. And no module undersrc/importsapi.
The second rule is what makes the first mean something: without it, a library module could reach a
web framework through the console package and the claim would read as true while being hollow. The
console may depend on the library; the library may never depend on the console. There is no ROS node
in this repository, and no module imports rclpy.
| Subsystem | Role |
|---|---|
core/ |
The vendor-neutral contract: RobotArm and Gripper Protocols, JointPositions, MotionResult and MotionStatus, the vendor enums, the RobotError hierarchy. The only part of this package a pipeline imports directly. |
drivers/ |
Lazy arm-driver registry (create_arm). Registered: ur, kuka, sim, dummy. franka and ros2 are named package slots with no driver. |
grippers/ |
Lazy gripper-driver registry (create_gripper): robotiq, onrobot, vacuum, jaw_io, dummy, none. The Isaac sim jaw and suction drivers sit outside the registry. |
safety/ |
The fail-closed SafetyPreflight guard pipeline in fixed order: workspace, joint limit, IK quality, self-collision, payload, motion continuity. It outranks every learned layer. |
grasping/ |
Candidate generation, scoring, the decision gate, refinement, verification, recovery, and the telemetry record. Also the replay and KPI gate and the offline reinforcement-learning seam. |
execution/ |
The composition root and the operator-facing services: Cell, RuntimePickService, AutonomousGraspService, the real_cell entry point, calibration and IK services. |
perception/ |
The live-camera PerceptionSource for real hardware. It lives here because the dependency edge only ever runs from robot to camera. |
Three loose modules sit beside them. constants.py holds the log file names every subsystem writes
to plus HOME_JOINTS_DEFAULT, the looking-down home configuration for a 6-axis UR. events.py
defines RobotCalibrationEvent and RobotWatchdogEvent (drift, out-of-distribution, degraded mode,
blocked autonomy, latency budget breach) with their listener Protocols. __init__.py re-exports the
vendor-free names eagerly and resolves the UR-bound ones lazily on first attribute access, so
importing this package does not pull in the UR driver chain.
- Perceive. A
PerceptionSourceframe plus a resolver for the camera-to-base transform. - Generate and score.
GraspCalculatorproduces 6-DoF candidates and ranks them. - Decide. The
DecisionEngineautonomy gate. - Refine. A bounded second look at the chosen candidate.
- Gate.
SafetyPreflightplus IK. - Drive. Standoff, approach, grasp, gripper close.
- Verify. Width delta, object detection, or vision.
- Recover. Rescan, next viewpoint, push.
- Log. One
GraspAttemptRecordper attempt, as JSON lines.
Steps 3, 4, 7 and 8 are opt-in and off by default. The shipped default pick is 1, 2, 5, 6, 9, and it is open-loop. Turning a step on is a config change, not a code change, and a switch that would read as on while doing nothing is refused by the schema rather than accepted quietly.
Offline, the logged records feed the replay and KPI gate and the reinforcement-learning training loop. Both sit downstream of the telemetry record and are never imported back into the live path.
from src.robot import ... gives four tiers.
Eager, vendor-free: RobotArm, Gripper, JointPositions, RobotCapabilities; the vendor enums
RobotVendor (ur, kuka, franka, ros2, sim, dummy) and GripperVendor (robotiq,
franka_hand, schunk, vacuum, jaw_io, onrobot, dummy, none); the error hierarchy
RobotError with RobotConnectionError, RobotKinematicsError, RobotMotionRejected,
RobotSingularityRisk and RobotEmergencyStop; and the constants above.
Lazy, resolved on first access: the UR facade Robot and URRobotArm, URConnection, URPose;
GripperController; the safety guards with SafetyPreflight, SafetyContext, SafetyDecision and
SafetyReason; MotionController, PoseProvider, CalibrationRoutine and CalibrationResult.
Registries: create_arm(vendor, **kwargs), register_arm_driver and available_vendors from
drivers/; create_gripper(vendor, **kwargs) and register_gripper_driver from grippers/.
Facades from execution/: RuntimePickService returning a PickSessionReport, and
AutonomousGraspService with a typed GraspMode (easy, auto, closed_loop, dense_clutter,
dense_autonomous), built by from_robot_config(...) or from_components(...), whose pick()
returns an AutonomousGraspReport.
RobotArm fixes the vendor contract: connect, disconnect, is_connected, get_tcp_pose() and
get_joint_positions(), fk and ik, move_joint, move_linear, stop, and the typed
move(pose, ...) -> MotionResult. Gripper fixes activate, set_width_mm, get_width_mm and the
width bounds; ObjectDetectingGripper adds the opt-in is_object_detected() used for post-close
verification.
If the safety rejection rate rises in a replay rollup, one of these guards is the cause. Never
disable a guard to clear the rate; fix the input that made it fire.
On call, the procedures live in the runbooks under docs/runbooks/:
real_cell_first_pick.md for a cell being taken to
metal, ur3e_cell_bringup.md for one moving to a robot
the stack has not run before.
| Guard | Symptom | What to do |
|---|---|---|
workspace |
The target pose is outside the configured workspace box. | Confirm robot.workspace_limits and robot.safety.limits.workspace_margin_mm, then verify the camera-to-base calibration. Reject the target. |
joint_limit |
The IK solution would drive an axis past its window. | Change the approach angle or prefer another IK seed. Do not widen the limits. |
ik_quality |
IK returned a high-residual or near-singular solution. | Re-rank toward feasible candidates and require a minimum IK quality. |
self_collision |
The commanded configuration puts a link, the tool or a fixture closer than min_distance_mm. |
Take another approach through the next-viewpoint recovery, or tune the inflation margin. |
payload |
The declared payload is outside the envelope. | Correct the payload estimate. Refuse the pick if the real mass is higher. |
motion_continuity |
Successive targets imply a step larger than the cap. | Rank toward joint-continuous candidates and review trajectory blending. |
Every guard reports through MotionResult and GraspAttemptRecord, so the replay harness rolls the
rejections up without any extra wiring.
from src.config.loader import load_config
from src.robot.core import RobotVendor
from src.robot.drivers import create_arm
cfg = load_config()
with create_arm(RobotVendor.from_string(cfg.robot.vendor), config=cfg.robot) as bot:
pose = bot.get_tcp_pose() # Pose in Frame.BASE, millimetres and XYZW
bot.move_linear(target_pose) # raises on an IK or frame faultThis package is a library. The command-line entry points live under execution/, grasping/,
safety/planning/, drivers/ur/ and perception/:
# config, preflight, build, connect, pick. `console_dummy` is the desk profile whose gripper a
# dummy arm can carry; without it the base tree's Robotiq substitutes and the connect is refused.
python -m src.robot.execution.real_cell --rehearse --runs 3 --profile console_dummy
python -m src.robot.safety.planning --check # are the external motion engines wired
python -m src.robot.grasping.replay --records run.jsonl # roll up KPIs from a record log
python -m src.robot.grasping.replay --soak-report # the soak gate, exit 0 iff it passes
python -m src.robot.grasping.rl build-dataset --dataset-id=v1_bootstrapUnits are millimetres, rotations are XYZW quaternions, and every pose and transform carries its frame. Vendor unit conventions stay inside the driver boundary.
The default pick is open-loop. The autonomy gate, closed-loop refinement, verification and recovery, multi-view fusion, the learned success model and the reinforcement-learning layer are all built and shipped with their config blocks disabled. Reading the code is not enough to know what a given cell runs; read its config.
from_robot_config() is the real-hardware boot path and python -m src.robot.execution.real_cell is
its caller. --rehearse drives the whole path on a dummy arm. The path has never been run against a
physical robot controller, and it refuses to build a real cell that has no camera-to-base resolver.
The KUKA driver is registered but has never driven a physical controller. Its payload writes are config-only, arm-against-arm self-collision is not modelled for it, and its joint limits must be set explicitly in config or the joint-limit guard reports itself unavailable and fails closed.
franka/ and ros2/ are package slots holding a docstring and an empty __all__. No driver class
exists and nothing is registered. GripperVendor.FRANKA_HAND and GripperVendor.SCHUNK are
likewise names with no real-hardware driver behind them.
MotionResult.from_bool labels a bare False as CONTROLLER_REJECTED. That is the most common
cause for a driver that returns a bool with no classification, but a caller that already knows the
real cause must pass failure_status= rather than accept the default.
The reinforcement-learning layer is additive and off. It is trained offline, gated by off-policy
evaluation before promotion, and shadow-only at runtime. It can reorder or filter candidates the
safety layer has already cleared and it can never override a rejection. A cell that leaves
robot.rl.mode alone imports no reinforcement-learning module at runtime.
core/for the contract every pipeline importssafety/for the guard pipeline, anddocs/safety-math.mdfor its geometrygrasping/for the generation and telemetry machineryexecution/for the composition root and the pick servicesdocs/guide/04-robot-and-safety.mdfor the walkthrough from an empty directory to a gated move