Teleoperation¶
Eden can be driven by a person instead of a policy: a keyboard, gamepad or SpaceMouse on the desktop, a GELLO leader arm, a Meta Quest or PICO headset, a Wuji Glove, or the SONIC whole-body pipeline for a Unitree G1. This page covers what each input path needs, how its process layout looks, and how its readings reach the env's action terms. To save what you teleoperate as a dataset, see Datasets and recording.
Choosing an input path¶
| Input | Process layout | Command | Install | Start from |
|---|---|---|---|---|
| Keyboard, gamepad | in the sim process | — | all extra (pynput, inputs) |
examples/recipes/teleop/keyboard/, examples/recipes/teleop/ps4/ |
| SpaceMouse | in the sim process | — | the spnav package (no extra declares it) |
examples/recipes/teleop/spacemouse/ |
| On-screen joint sliders | in the sim process | — | vis extra (imgui-bundle) |
examples/recipes/teleop/gui/teleoperation_gui_franka.py |
| GELLO leader arm | leader process → sim over ZMQ | eden gello calibrate, eden gello leader |
gello extra + dynamixel-sdk |
examples/recipes/teleop/gello/ |
| Meta Quest (WebXR) | bridge process ↔ sim over ZMQ | eden metaquest bridge |
metaquest extra + metaquest-git group |
examples/recipes/teleop/metaquest/ |
| PICO (XRoboToolkit) | bridge process ↔ sim over ZMQ | eden pico bridge |
pico extra + a locally built xrobotoolkit_sdk |
examples/recipes/teleop/pico/ |
| Wuji Glove | in the sim process (no bridge) | eden wuji scan, eden wuji monitor |
wuji extra (Linux only) |
examples/recipes/teleop/wuji/ |
| SONIC whole-body (G1 + PICO) | operator process → robot process over ZMQ | eden teleop sonic operator, eden teleop sonic robot |
sonic + onnx or onnx-gpu extras, sonic-viz for the operator views |
examples/recipes/teleop/sonic/README.md |
Every recipe folder above is runnable as-is and lists its options with --help; eden examples list --capability
teleop.gello (or teleop.keyboard, teleop.metaquest, …) finds them by capability. The headset and glove
folders carry a README.md with the full hardware setup; this page summarizes them and points at the parts that
are easy to get wrong.
Desktop devices¶
The device contract¶
Every desktop device subclasses DeviceBase (eden/utils/devices/base.py):
get_command()returns the current reading as atorch.Tensor(on the CPU; move it toenv.device).add_callback(key, fn)registersfn(on_press: bool)for a key or button, called on press and on release.reset()clears the reading.
The device only produces numbers. Turning them into motion is the job of the env's action terms, so a teleop loop is a few lines:
# excerpt from examples/recipes/teleop/keyboard/teleoperation_keyboard.py
from eden.utils.devices.keyboard import SE3Keyboard
keyboard = SE3Keyboard()
keyboard.add_callback("x", lambda on_press: env.reset() if on_press else None)
while True:
action = keyboard.get_command() # (7,): 6-D delta pose + gripper toggle
env.step({
"ee_controller": action[:6].unsqueeze(0).repeat(env.num_envs, 1).to(env.device),
"gripper_controller": action[6:].unsqueeze(0).repeat(env.num_envs, 1).to(env.device),
})
In that recipe ee_controller is a DifferentialIKController (or OperationalSpaceController) configured with
position_scale=0.05 and orientation_scale=0.05, and gripper_controller is a BinaryJointController. See
Actions for both.
Device readings are normalized
SE3Keyboard(pos_max=1.0, rot_max=1.0) emits a delta pose in [-1, 1] per axis. The metres and radians
come from the task-space controller's position_scale / orientation_scale. Lowering pos_max clamps the
device below full deflection; the physical step is then the product of the two.
Turn off the viewer's own key bindings
The keyboard recipes build the env with EnvOptions(enable_default_keybinds=False), so the Genesis
viewer's built-in shortcuts do not fire on the same keys the teleop device listens to.
Keyboard¶
SE3Keyboard (eden/utils/devices/keyboard.py) has two layouts: SE3Keyboard.default_key_bindings() (the
default) and SE3Keyboard.wasd_key_bindings(); pass one as key_bindings=.
| Command | Default (+ / −) | WASD layout (+ / −) |
|---|---|---|
| Move along x | 8 / 2 |
w / s |
| Move along y | 4 / 6 |
a / d |
| Move along z | 7 / 9 |
q / e |
| Rotate about x | Up / Down | Up / Down |
| Rotate about y | Right / Left | Right / Left |
| Rotate about z | 1 / 3 |
z / c |
| Toggle gripper | Space | Space |
Held keys ramp up through tanh, so the reading approaches full deflection as key-repeat accumulates.
pynput listens to the whole desktop, and needs one
pynput reads the keyboard globally, not the focused window, so keys you type into a terminal reach the
device too. It also needs a local display: over a plain SSH shell the import fails with a message saying so.
SE(2) velocity input for locomotion¶
For a walking robot, TeleopSE2Keyboard and SE2Gamepad emit a normalized (vx, vy, wz) deflection.
eden.utils.devices.se2.make_se2_device("keyboard" | "gamepad", options) builds one, and the
OperatorVelocityCommand command term reads it every control step, scaled and clamped into the term's
lin_vel_x_range / lin_vel_y_range / ang_vel_z_range. Substitute it for a trained UniformVelocityCommand
and the robot goes where you steer it, never faster than it was trained for. It reads a physical device, so it
is for deployment and interactive evaluation, not training. See Commands.
make_se2_device("keyboard") binds these keys:
| Command | Key (+) | Key (−) |
|---|---|---|
Forward / backward vx |
Up | Down |
Strafe vy |
Left | Right |
Turn wz |
, |
. |
The keyboard bindings avoid letters and digits on purpose: eden deploy reads s (START), x (STOP), i
(INIT), k (KILL) and digits from the terminal, and a global listener on WASD would turn typing START into a
backwards command. A held key is full deflection from the first press (TeleopSE2Keyboard, not the tanh-ramped
SE2Keyboard), so the commanded speed does not depend on your key-repeat rate. A TeleopSE2Keyboard() built
directly keeps SE2Keyboard's numpad layout (8/2, 4/6, 7/9); pass key_bindings=teleop_key_bindings() (from
eden.utils.devices.se2) to get the table above. The gamepad layout is
options={"layout": "ps4"} (default) or "xbox".
Gamepad, SpaceMouse and sliders¶
SE3Gamepad/SE2Gamepad(eden/utils/devices/gamepad.py, theinputspackage) — PS4 and Xbox axis scaling are built in (SCALE_PS4,SCALE_XBOX). Button callbacks use theinputsevent names, e.g.gamepad.add_callback("BTN_EAST", ...)inexamples/recipes/teleop/ps4/teleoperation_ps4.py.SE3Spacemouse/SE2Spacemouse(eden/utils/devices/spacemouse.py) — read throughspnav.SliderGUI(eden/utils/devices/gui_slider.py) — one ImGui slider per DOF in the Genesis viewer plus a Reset button. Build the env withshow_viewer=True, then passviewer=env.scene.viewer,dofs_name,dofs_pos_limits(an(n_dofs, 2)tensor; infinite limits are clipped to ±π) and optionallyreset_callback=env.reset.
GELLO leader arms¶
GELLO is a low-cost leader arm built from Dynamixel servos. You move the leader by hand; its joint angles drive the simulated follower. The leader and the sim are two processes joined by a ZMQ request/reply socket:
flowchart LR
L["eden gello leader<br/>(GelloLeader, Dynamixel bus)"] -- "command_joint_state (ZMQ REQ/REP)" --> S["GelloZMQServer<br/>inside your Eden script"]
S --> E["env.step(...)"]
Install¶
pip install 'eden-world[gello]' # or, in a checkout: uv sync --inexact --extra gello
pip install dynamixel-sdk # the servo driver; no extra declares it
1. Calibrate the leader (once per leader)¶
Hold the leader at a known joint configuration and pass it as --start-dofs (radians):
eden gello calibrate \
--port /dev/ttyUSB0 \
--start-dofs 0 0 0 0 0 0 \
--signs 1 1 -1 1 1 1 \
--gripper \
--output configs/gello_ur5e.yaml
It prints a ready-to-paste GelloLeaderConfig(...) and, with -o/--output (.json, .yaml or .yml), saves
it. Other flags: --joint-ids (Dynamixel IDs of the arm DOFs; default 1..N), --gripper-id (default N+1),
--baudrate (default 57600; high-speed boards use 1000000), --warmup-reads, --max-pi-multiple,
--gripper-open-delta-deg / --gripper-close-delta-deg (shifts applied to the current gripper reading to
estimate its open and closed ends).
2. Start the follower in Eden¶
GelloZMQServer (eden/utils/devices/gello/zmq_server.py) wraps a built env and serves its arm (and optional
two-DOF gripper) over ZMQ. Its reading is the latest joint target the leader sent:
# excerpt from examples/recipes/teleop/gello/teleoperation_gello_ur5e_robotiq.py
from eden.options.robots.robotiq_2f85 import Robotiq2f85
from eden.options.robots.universal_ur5e import UR5e
from eden.utils.devices.gello.zmq_server import GelloZMQServer
server = GelloZMQServer(
env=env,
arm_entity_name="arm",
arm_dofs_name=UR5e.actuated_dofs_name,
ee_link_name=UR5e.ee_links_name[0],
gripper_entity_name="gripper",
gripper_dofs_name=Robotiq2f85.actuated_dofs_name,
host="127.0.0.1",
port=6001,
)
try:
server.start()
while True:
cmd = server.get_command().to(env.device)
env.step({
"arm_dof_controller": cmd[: len(UR5e.actuated_dofs_name)].unsqueeze(0),
"gripper_controller": cmd[len(UR5e.actuated_dofs_name):].unsqueeze(0),
})
finally:
server.stop()
In the recipe the arm term is an ImplicitPDController with reference_source=ReferenceSource.ZERO and
scale=1.0 (absolute joint targets), and the gripper term a BinaryJointController between the gripper's
open_dofs_pos and close_dofs_pos. Recipes exist for a UR5e + Robotiq 2F-85, a WidowX-250s and a bimanual
ALOHA.
Pass the port explicitly
GelloZMQServer defaults to port=5001, while eden gello leader and GelloClient default to 6001. The
recipes pass --port 6001 to the server; do the same in your own script, or the leader connects to nothing.
3. Run the leader¶
eden gello leader -c configs/gello_ur5e.yaml --port /dev/ttyUSB0 --zmq-host 127.0.0.1 --zmq-port 6001
It sends the calibrated joint state at 100 Hz until Ctrl-C, after checking that the server's DOF count equals the leader's. The same loop in Python:
import time
from eden.utils.devices.gello import GelloClient, GelloLeader, GelloLeaderConfig
config = GelloLeaderConfig.load("configs/gello_ur5e.yaml")
leader = GelloLeader(port="/dev/ttyUSB0", config=config, start_dofs=[0.0] * config.num_dofs)
client = GelloClient(host="127.0.0.1", port=6001)
try:
while True:
client.command_joint_state(leader.get_command())
time.sleep(1 / 100)
finally:
leader.close()
client.close()
What the leader sends:
- Each arm DOF is
(raw − offset) · sign, in radians. The gripper, when configured, is normalized to[0, 1]with0open and1closed. start_dofsre-aligns the offsets by whole turns (multiples of 2π) so that the leader reads close to that configuration at start-up.eden gello leaderpasses all zeros, so start the leader near its zero pose.get_offset(current_dofs, start_dofs, signs)(from eden.utils.devices.gello import get_offset) is the module-level helper the calibration uses; it searches offsets in multiples of π/2.
GelloClient exposes the server's RPCs:
| Method | Returns |
|---|---|
num_dofs() |
int (arm DOFs + 1 with a gripper) |
get_joint_positions() |
np.ndarray: arm positions, plus the normalized gripper position (0 open, 1 closed) |
get_observations() |
dict with joint_positions, joint_velocities, ee_pos_quat (world frame, wxyz) and gripper_position |
command_joint_state(joint_state) |
None; the server rejects a wrong length or non-finite values |
Meta Quest (WebXR)¶
A Meta Quest opens a WebXR page that eden metaquest bridge serves over Wi-Fi (through televuer and its Vuer
server). The bridge publishes the headset's tracking and shows the stereo frames your Eden script renders:
Meta Quest ──WebXR/Wi-Fi──▶ eden metaquest bridge
│ poses (ZMQ PUB :5557) ──▶ your Eden script
◀ frames (ZMQ PULL :5556) ──┘ (renders a stereo camera pair)
Install the extra together with televuer, which is not on PyPI:
Outside a checkout, the bridge's import error prints the equivalent pip install command with the pinned
televuer git requirement.
Run the bridge in one terminal and the sim in another:
eden metaquest bridge # hand tracking (default)
eden metaquest bridge --tracking controller # motion controllers instead
uv run python examples/recipes/teleop/metaquest/demo.py # hand-tracking recipe
uv run python examples/recipes/teleop/metaquest/controller_demo.py # controller recipe
The bridge prints the URL to open on the headset (https://<host-ip>:8012/?ws=wss://<host-ip>:8012). WebXR
needs HTTPS: the headset must accept a TLS certificate whose SAN covers the host's LAN address (a self-signed
pair works; accept it once in the headset browser).
Bridge flags: --width (default 1280, both eyes) / --height (480), --fps (30), --display-mode
(immersive, ego or pass-through) and --tracking (hand or controller).
eden metaquest bridge --help prints Vuer's options
The televuer import parses the command line itself, so --help lists Vuer's server settings rather
than the bridge's flags. The flags above are the ones the bridge reads (eden/utils/devices/metaquest/xr_bridge.py).
What the bridge publishes:
- WebXR treats hands and controllers as separate input sources, so the bridge runs one mode at a time and every
published dict carries
"mode": "hand"or"mode": "controller". - Hand mode carries head and wrist poses, 25 hand-joint positions, and pinch/squeeze. Controller mode carries head and wrist (grip) poses, per-hand trigger, grip, A/B buttons and thumbstick.
- Poses are already in the Genesis world frame (x forward, y left, z up, metres, origin at the XR tracking origin), so the sim side uses them directly.
- Both processes share
eden.utils.devices.metaquest.protocol(POSE_PORT = 5557,FRAME_PORT = 5556, keep-latest sockets), so ports and framing have one definition.
For arms, ClutchPoseMapper (eden/utils/devices/metaquest/pose_mapping.py, ported from vr-teleop-kit) turns a
controller pose into an end-effector target. Hold the grip to engage and the target follows your hand's
relative motion; release to reposition your hand without moving the robot. When you pass the arm's current
end-effector pose to mapper.target(...), motion past the mapper's reach limit is absorbed (a "slipping clutch")
instead of building up error the arm cannot follow. examples/recipes/teleop/metaquest/controller_teleop.py
drives two YAM arms this way: controller pose → ClutchPoseMapper.target → RigidEntity.inverse_kinematics →
control_dofs_pos, with the triggers on the grippers.
To teleoperate a registered task instead of the recipe's toy scene, build it with
RLEnvBase.from_config(cfg), add a stereo camera pair through the config's cameras_options, and keep the
recipe's pose → action and render → stream loop.
PICO (XRoboToolkit)¶
A PICO 4 or PICO 4 Ultra streams its tracking to the XRoboToolkit PC Service on the host. eden pico bridge
reads that service and drives the headset's Remote Vision video link:
PICO ──Wi-Fi──▶ XRoboToolkit PC Service ──▶ eden pico bridge
│ poses (ZMQ PUB :5557) ──▶ your Eden script
headset decoder ◀── H.264 / TCP ──────────┤ frames (ZMQ PULL :5556) ◀──┘
└ control server TCP :13579
The bridge publishes the same dict schema and ZMQ channels as eden metaquest bridge --tracking
controller, so the Meta Quest controller recipes run unchanged against a PICO. PICO adds keys:
{side}_menu_button, trackers (up to 5 motion trackers), body_ready / body (the 24 SMPL-ordered joints in
Full-body mode), and body_raw / body_raw_timestamp_ns (the untouched service-frame poses).
Setup:
uv sync --inexact --extra pico(orpip install 'eden-world[pico]'): PyAV for the H.264/H.265 encoder and pyzmq.- Install the XRoboToolkit PC Service on the host (see XR-Robotics).
- Build the
xrobotoolkit_sdkPython module fromXRoboToolkit-PC-Service-Pybindwith Eden's interpreter active (bash setup_ubuntu.sh). It is built against the service'slibPXREARobotSDK.so, so no index can ship it and no extra declares it; the bridge fails at start-up with this hint when it is missing. The library is preloaded from/opt/apps/roboticsservice/SDK/x64, so noLD_LIBRARY_PATHexport is needed; setEDEN_PXREA_SDK_DIRto search another directory first. - Install the XRoboToolkit app on the headset, connect it to the host, select Head + Controller tracking, enable Send, and tap Listen under Remote Vision.
eden pico bridge
uv run python examples/recipes/teleop/pico/demo.py # GPU; --backend cpu for CPU
uv run python examples/recipes/teleop/pico/controller_teleop.py # bimanual YAM arms
Bridge flags: --fps (pose publish rate), --control-address (default 0.0.0.0:13579), --pose-port,
--frame-port, --no-video (tracking only), --no-derived-body (publish body_raw only, skipping the per-joint
body dict and the trackers) and --tracker-role SERIAL=ROLE (repeatable).
Label your motion trackers
The service reports trackers by serial number only. Run the bridge once: it logs each unlabeled serial. Then
map them, e.g. eden pico bridge --tracker-role PA12345=waist --tracker-role PB67890=left_foot, and
state()["trackers"]["waist"] is keyed by role.
Hand tracking is not index-compatible with the Quest recipe
PicoXrClient.hand_joints() exposes PICO's 26-joint OpenXR hand layout, but the bridge publishes controller
state only. The Quest hand recipe assumes WebXR's 25-joint layout.
examples/recipes/teleop/pico/README.md covers the in-headset steps, the control-frame options of demo.py
(gaze-relative or room-fixed clutch, --viewer-camera) and firmware quirks.
Wuji Glove¶
The Wuji Glove needs no bridge: WujiGlove (eden/utils/devices/wuji/glove.py) talks to wuji_sdk in your
process over USB or Ethernet, keeping the newest frame of each subscribed stream.
uv sync --inexact --extra wuji # Linux only: wuji-sdk ships manylinux wheels
eden wuji scan # list the gloves on the bus
eden wuji monitor --side right --seconds 30
eden wuji monitor prints, once per tick, whether the glove is solving a pose, each fingertip's distance from
the wrist, and the tactile sheet's active-taxel count and peak. Its flags: --sn, --side {left,right},
--streams ..., --rate-hz (default device rate 120 Hz), --seconds (0 runs until Ctrl-C) and --exclusive.
from eden.utils.devices.wuji import WujiGlove
glove = WujiGlove(side="right", streams=("hand_skeleton", "tactile"), rate_hz=60)
keypoints = glove.get_command() # (21, 3) landmarks in Eden's hand frame; zeros before the first frame
pressure = glove.tactile() # (24, 31) sheet in [0, 1]; dead taxels are nan; None before the first frame
glove.close()
Subscribe only to the streams you use (STREAMS lists them; the default is hand_skeleton, hand_joint_angles
and tactile): each one is another callback into Python. Every getter returns None until its stream's first
frame, so a loop can hold its last pose instead of blocking.
Telemetry
Importing wuji_sdk starts a background worker that sends anonymous usage events to a third-party analytics
host. Set WUJI_TELEMETRY_DISABLE=1 to turn it off. Eden does not set it for you.
Frames: the glove reports the hand relative to the wrist, not the room — pair it with an arm, headset or
tracker for a world-frame wrist pose. Eden re-expresses the vendor's anatomical wrist frame as a hand frame with x
distal (along the fingers) and z dorsal (out of the back of the hand), so an axis means the same direction on
either glove; pass frame="wuji" to a getter for the vendor's raw numbers.
Driving a robot hand from it (examples/recipes/teleop/wuji/):
glove_demo.pydraws the skeleton and the tactile pressure, with no robot.glove_teleop.pydrives a simulated Wuji Hand 2.--retargeter wuji(default) uses the vendor'sRetargetSessionfor that hand;--retargeter dexpilotuses Eden'sDexRetargeter(needs thedex-retargeterextra), which matches fingertip vectors only and leaves PIP/DIP under-constrained on a human-sized hand. Preferwujifor the Wuji Hand 2;dexpilotis the route to other hands (--preset). See Dex retargeting.glove_real_hand.pydrives a real Wuji Hand 2 throughWujiHandDeployment;--dry-runretargets without connecting the hand. See Deployment before running it.
Glove gotchas:
tactile_binaryandtactile_residualread zero until you run the vendor's per-glove tactile calibration; plaintactileneeds none. Rawtactileis not contact: an unworn glove reads well above zero across much of the sheet.- The hardware pins dead taxels to
-1.0; Eden's getters turn them intonan, so reduce withtorch.nanmean.state()keeps the raw-1.0, becausenanis not valid JSON. - The active-taxel count is
N_ACTIVE_TAXELS(526), not the number of non-sentinel cells (N_UNMASKED_TAXELS, 534). hand_joint_anglesare the human hand's 21 DOFs, not a robot's. Retarget fromkeypoints().
SONIC whole-body teleoperation¶
eden teleop sonic runs Eden's port of NVIDIA's GR00T-WholeBodyControl teleop stack: a PICO 4 Ultra (headset,
two controllers, two ankle trackers) drives a Unitree G1 under the SONIC policy. Two processes speak the
reference's byte-compatible ZMQ protocol:
PICO ─Wi-Fi→ XRoboToolkit PC Service
└→ eden pico bridge (spawned by the operator, pose PUB :5559)
└→ eden teleop sonic operator ──ZMQ :5556 (pose / planner / command)──→ eden teleop sonic robot
←──ZMQ :5557 (g1_debug feedback)─────────┘ (G1 body + SONIC + planner)
pip install 'eden-world[sonic,onnx-gpu]' # onnx for CPU inference; add sonic-viz for --vis-vr3pt / --vis-smpl
eden teleop sonic robot --vis # T1: Genesis G1 + SONIC policy + planner
eden teleop sonic operator --vis-vr3pt --vis-smpl # T2: PICO tracking; --robot-host <ip> across machines
The operator also needs xrobotoolkit_sdk (see PICO). By default it spawns the PC Service
and a pose-only bridge; --no-service-start, --no-bridge-start and --bridge-host change that, and --sdk
reads the SDK in-process. The G1 model and the SONIC encoder/decoder/planner ONNX files download on first use;
the robot process (eden teleop sonic robot) takes --encoder-path / --decoder-path / --planner-path to use
local files instead.
Operator modes, selected on the PICO with the button combos below (each on its rising edge, except the menu
pause). The names are the ones the operator prints. The table is eden/extensions/teleop/sonic/state_machine.py:
| Mode | Entered by | Left by | What moves |
|---|---|---|---|
PLANNER |
A+B+X+Y from OFF; A+X from POSE; a stick click from the PLANNER_VR_3PT it opened |
A+X → POSE; left-stick click → PLANNER_VR_3PT |
joystick locomotion |
POSE |
A+X from PLANNER; B+Y from PLANNER_FROZEN_UPPER_BODY; A+X or B+Y from PLANNER_VR_3PT; releasing the menu in POSE_PAUSE |
A+X → PLANNER; B+Y → PLANNER_FROZEN_UPPER_BODY; left menu held → POSE_PAUSE |
whole body from the SMPL body pose |
PLANNER_FROZEN_UPPER_BODY |
B+Y from POSE; a stick click from the PLANNER_VR_3PT it opened |
B+Y → POSE; left-stick click → PLANNER_VR_3PT |
planner legs, arms frozen where they were |
PLANNER_VR_3PT |
left-stick click from PLANNER or PLANNER_FROZEN_UPPER_BODY |
left-stick click → back to that mode; A+X or B+Y → POSE |
arms follow your hands, legs walk on the planner |
POSE_PAUSE |
holding the left menu button in POSE |
releasing it → POSE |
nothing new: the operator stops streaming poses |
A+B+X+Y disengages (→ OFF) from every mode.
A+B+X+Y while engaged disengages: the robot is handed to the planner's idle and keeps balancing, unlike the
reference stack, whose stop ends the robot process with a damping-only command. Robot-terminal keys: 9
toggles the elastic band, 7/8 set its length, o is the e-stop, i re-captures the heading. A headless
robot host has no keyboard, so use --drop-band-after SECONDS.
The robot process has three bodies. The default is the Genesis simulator. --mujoco (sim-to-sim) and --real
(the real G1 over DDS, which needs unitree_sdk2py) drive the body-only 29-DOF G1 through the eden deploy
backends: the Dex3 hands are not driven, the deployment's own STOP / INIT / START sequence comes first, and the
deployment flags keep their eden deploy spelling (--deploy_kp_kd_scale, --target_lpf, --disable_joints,
--debug_record, --debug_record_path). --joint-vel-limit (default 35 rad/s, --real only) stops the session
when any body joint exceeds it. --auto-reset (Genesis and --mujoco only) resets the session to the launch state
when the robot falls with the band off.
--real is uncommissioned
The --real path has been exercised only against an in-memory wire, not on a robot. Commission it at a low
--deploy_kp_kd_scale, with the robot supported by a gantry. See Deployment.
Port collision with a standalone PICO bridge
A default-port eden pico bridge (frames :5556, poses :5557) collides with the SONIC wire (:5556 / :5557).
The bridge the operator spawns uses --no-video --pose-port 5559. When you run one yourself, use those flags
(and --no-derived-body --fps 120) and start the operator with --no-bridge-start.
examples/recipes/teleop/sonic/README.md holds the full procedure: the calibration pose, engaging POSE,
recalibrating VR_3PT, the health lines both processes print, and every control.
Writing an operator stack¶
eden.extensions.teleop holds the interfaces an operator-station stack implements; the SONIC stack under
eden/extensions/teleop/sonic/ is the reference implementation of all three.
TeleopOperator— the operator lifecycle, configured byTeleopOptions(robot_host,port,feedback_port; device-specific fields arrive as extras).run()callssetup(), thenstep()untilshould_stop(), and alwaysteardown(), which must cope with asetup()that failed part-way.TeleopInputSource— a tracked-input reader withstart,stop,get_latest(Nonebefore the first frame),get_timestamp_ns,controllersand adisconnectedproperty.TeleopStreamer— one control mode's per-tick pipeline:run_once(...)publishes at most one message per tick and paces itself;reset_yaw()runs on mode entry,on_mode_exit()on exit.
TeleopInputSource and TeleopStreamer are plain ABCs because they are constructed with live sockets and
callables, not from an options schema.
See also¶
- Datasets and recording — record a teleoperated session to HDF5, then LeRobot.
- Actions — the IK, OSC, PD and binary gripper terms a device drives.
- Commands —
OperatorVelocityCommand. - Dex retargeting — mapping a human hand onto a robot hand.
- Deployment — running on the real robot.