Robots

Pick a built-in by key in a config, or register a new Robot subclass.

Pick your path first

SituationActionSections
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 listWrite one Robot subclass and register it.All sections

Prerequisites

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:

configs/01_deploy/ur5e/_base.py
# _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)
)
KeyMeaningChange?
robot.typeRegistered key of the robot to run.Yes.
transport.typezmq (most setups), ros1/ros2, or dataset (replay).Usually zmq.
transport.resize_padPad camera images to a square.Rarely.
transport.image_layoutPixel 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.

Optional gripper and frame keys from the shared defaults: 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:

bash
# 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
Set the robot address first. 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.

Vendor SDKs live in the hardware node, not EVA. EVA itself only speaks 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.

KeyLayoutAction size · notes
agilex_AGILEXTwo 6-DoF arms14 values; gripper at link gripper_base
arx_r5Two 6-DoF arms14 values; gripper at link link6
ur5eOne 6-DoF arm7 values; gripper at link grasp_link
dual_frankaTwo 7-DoF Panda arms16 values; frames workcell/base_link
r1_liteTwo 6-DoF arms on a fixed torso14 values; frames base_link/torso_link3
agibot_g2Humanoid: two 7-DoF arms + 3-DoF head + 5-DoF torso24 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.

text
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
Transport stays out of the robot class. ROS topics, ZMQ endpoints, and dataset columns live in the transport backend, so one robot class drives any transport unchanged.

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].

FieldMeaning
nameGroup name, referenced elsewhere in the class.
dofNumber of action values this group consumes.
joint_namesJoints in URDF order.
gripper_indexIndex 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:

SegmentEffect
copy a rangePass action values through — arm joints.
fixed valuesHold joints at constants — locked wheels, fixed torso.
gripper mappingExpand 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.

Reference frames are opt-in. A robot only accepts 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.

src/robots/zoo/<your_robot>/__init__.py
@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
Copy a built-in, don't hand-write the fields. 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:

src/robots/__init__.py
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

For the deeper public APIs, see Development.