Robots
Pick a built-in by key in a config, or register a new Robot subclass.
Pick your path first
| Situation | Action | Sections |
|---|---|---|
| Robot is one of the six built-ins (see the built-in list) | Point a config at it. No code. | Prerequisites, Wiring a config, The hardware side |
| Robot is new, not in the list | Write one Robot subclass and register it. | All sections |
Prerequisites
- EVA installed in a Python 3.10+ venv;
evaon PATH. See Installation. - EVA source checkout. All relative paths below (
configs/...,examples/...) are from the repo root; run commands there. - A reachable policy server. EVA connects out; it does not start the server. See Policy backends.
- The hardware node running on the robot's computer. EVA never drives motors directly — see The hardware side.
Wiring a config to a built-in robot
A config names the robot; at launch EVA looks the name up and constructs it. The name is the registered key — a literal string from the built-in list (e.g. ur5e).
Configs are layered: a base file sets the robot and transport; a leaf file on top adds the policy and action space. Start from configs/01_deploy/ur5e/_base.py and edit a couple of values:
# _base_ chains in shared defaults — leave this line as-is. _base_ = ['../../00_base/defaults.py'] robot = dict( type='<robot-key>', # ← one of the built-in keys, e.g. 'ur5e' ) transport = dict( type='zmq', # how EVA talks to the robot: zmq / ros1 / ros2 / dataset resize_pad=False, # keep default unless your camera images need padding image_layout='hwc', # your camera image order; 'hwc' or 'chw' (see Transport page) )
| Key | Meaning | Change? |
|---|---|---|
robot.type | Registered key of the robot to run. | Yes. |
transport.type | zmq (most setups), ros1/ros2, or dataset (replay). | Usually zmq. |
transport.resize_pad | Pad camera images to a square. | Rarely. |
transport.image_layout | Pixel order: hwc or chw. | Only if images look wrong. |
The leaf on top — e.g. configs/01_deploy/ur5e/openpi_qpos.py — adds the policy server and action space and ships with EVA. Full settings and layer-merge semantics are on the Configuration page; action-space choices are on the Action spaces page.
gripper_threshold (0.5), gripper_open/gripper_close (1.0/0.0), eef_reference_frame (base_link).The hardware side
EVA does not drive motors. The real-time loop talking to motors and cameras runs as a separate program on the robot's computer — the hardware node. EVA and the hardware node exchange observations and actions over the transport (usually ZMQ). Reference nodes live under examples/hardware/<robot>/; the UR5e one includes run_hardware.sh.
Run from the EVA repo root:
# On the robot's computer, in the EVA checkout: # 1) install the UR5e hardware drivers (one time) uv pip install -e ".[ur5e]" # 2) start the hardware node (talks to motors + cameras) bash examples/hardware/ur5e/run_hardware.sh # 3) start EVA against a ready-made UR5e config eva --config configs/01_deploy/ur5e/openpi_qpos.py
run_hardware.sh reads UR5E_ROBOT_IP, gripper serial port, and camera indexes from env vars; the defaults (e.g. 192.168.31.123) are the reference rig.Once both are up, EVA prints a console URL (default http://localhost:8080). The DEBUG page shows live camera feeds and a 3D model tracking the real pose. A joint-count or shape-mismatch error at startup means robot.type does not match the connected hardware.
zmq / ros1 / ros2 / dataset. URDF and mesh files ship inside the EVA install.The built-in robots
The Key column is the literal value for robot.type.
| Key | Layout | Action size · notes |
|---|---|---|
agilex_AGILEX | Two 6-DoF arms | 14 values; gripper at link gripper_base |
arx_r5 | Two 6-DoF arms | 14 values; gripper at link link6 |
ur5e | One 6-DoF arm | 7 values; gripper at link grasp_link |
dual_franka | Two 7-DoF Panda arms | 16 values; frames workcell/base_link |
r1_lite | Two 6-DoF arms on a fixed torso | 14 values; frames base_link/torso_link3 |
agibot_g2 | Humanoid: two 7-DoF arms + 3-DoF head + 5-DoF torso | 24 values |
If yours is listed, stop here and return to Wiring a config. The rest of this page covers adding a robot not in the list.
For developers: what a robot is made of
Adding a new robot means writing one Python class that describes the robot to EVA — joints, cameras, and how to draw it. The real-time motor loop is the hardware node, not this class.
Each built-in lives under src/robots/zoo/<name>/: an __init__.py with one Robot subclass next to an assets/ tree holding the URDF and meshes.
src/robots/zoo/<your_robot>/ ├── __init__.py # your Robot class + its registration └── assets/ └── <your_robot>_description/ ├── urdf/ │ └── <your_robot>.urdf └── meshes/ └── *.STL # 3D shapes the URDF points at
For developers: the parts you fill in
A robot class is built from a few description objects declared once on the class.
Actuator groups
An actuator group is one logical bundle of joints, such as "left_arm", with a fixed count and joint names in URDF order. The flat action vector is split across groups in declaration order — a two-arm robot is [left_arm, right_arm].
| Field | Meaning |
|---|---|
name | Group name, referenced elsewhere in the class. |
dof | Number of action values this group consumes. |
joint_names | Joints in URDF order. |
gripper_index | Index of the gripper value within the group, or none. |
Observation schema
The observation schema declares what the policy sees each step: it maps each camera's logical name to the key the policy reads (the transport later resolves logical names to topics or dataset columns), and lists which actuator groups' qpos get concatenated into the state vector.
3D-view config
The 3D-view config tells the console how to draw the robot: one part per URDF instance. Each part records its URDF, a world placement (position + quaternion), and the slice of joint positions driving it. Parts may share a URDF — a two-arm robot can place a single-arm URDF twice.
For developers: mapping joints to the 3D view
Each 3D part needs a mapping from action vector to drawn joints, given as an ordered list of segments:
| Segment | Effect |
|---|---|
copy a range | Pass action values through — arm joints. |
fixed values | Hold joints at constants — locked wheels, fixed torso. |
gripper mapping | Expand one gripper value into finger-joint motions, scaled to travel. |
These cover single-arm, two-arm-sharing-one-URDF, and whole-body layouts. For anything else, supply a mapping function directly.
For developers: arm motion (build_kinematics)
If the policy outputs end-effector poses, EVA needs IK to recover joint angles; it uses PyRoki and solves an entire chunk in one batch. If the policy outputs joint angles directly, the class reports no kinematics and EVA skips IK. Otherwise return a PyRoki solver per arm: ordered joints, the tip link, and optionally the supported reference frames.
eef_reference_frame if it declares the supported frames; an unsupported value errors at startup.For developers: write the class and register it
Subclass Robot, fill in the description objects, and register the class with @ROBOT_REGISTRY.register("<your-key>"). That key is what users put in robot.type. The UR5e source is the reference full example.
@ROBOT_REGISTRY.register('<your-key>') # ← the name users put in robot.type class YourRobot(Robot): def __init__(self): super().__init__( name='<your-key>', actuator_groups=..., # ← your joint groups (see "the parts you fill in") observation_schema=..., # ← your cameras + state vis_config=..., # ← your 3D-view parts ) def build_kinematics(self, **kwargs): return ... # a PyRoki solver, or None if no IK is needed
src/robots/zoo/ur5e/__init__.py spells out every group, camera, joint list, and initial pose with working values.Add the package to src/robots/__init__.py so the import triggers registration at startup:
from robots.zoo import ( agibot_g2, agilex_AGILEX, arx_r5, dual_franka, r1_lite, ur5e, your_robot, # ← add your folder name here )
The robot then behaves like a built-in: users select it via robot.type='<your-key>' as in Wiring a config.
For developers: which built-in to copy
- Single arm →
ur5e. - Two arms sharing one arm model →
agilex_AGILEX. - Whole body (arms on a torso or base) →
r1_lite.
For the deeper public APIs, see Development.