Skip to main content
Launches a VR teleoperation session. When started, the hostname (.local) and local IP address are printed — enter either of these in the VR app at axol.almond.bot to connect. Runs on the real robot by default; pass --sim for the browser visualizer. Hardware control always uses the required axol-rt realtime core, which owns CAN — at 240 Hz on the default impedance controller, or at 400 Hz with --axol.controller position, which runs every joint on its motor’s own firmware position loop (see controller in the robot config) — while Python handles VR, IK, and the slow model. The full robot config is exposed via draccus, so every gain, stiffness, and gripper limit is reachable from the CLI or a config file — see Command configuration for the syntax. The table below covers the commonly used flags; every field of the nested axol, teleop, kinematics, vr_server, and cart configs is listed with its default in Teleoperation Config Parameters.
For a guided, step-by-step walkthrough including the VR controller layout, see the Teleoperation guide.
Before opening the VR app, accept the self-signed HTTPS certificate in the VR browser by navigating to https://<hostname>.local:8000 or https://<local-ip>:8000 and proceeding past the security warning.
Network tip: If VR tracking feels jittery or packets arrive in bursts, configure the following on your router/access point:
  • DTIM interval → 1
  • Beacon interval → 100 ms
  • WMM APSD (U-APSD) → disabled
These settings prevent the AP from buffering packets between beacon intervals, which causes intermittent delivery delays that are especially noticeable for latency-sensitive VR traffic.
To verify the effective loop rather than trusting its console log, leave the Diagnostics dashboard open while teleop runs. Its passive CAN timing graph measures command/feedback rate, jitter, response latency, and missed 240 Hz deadlines with kernel timestamps.

Camera streaming to the headset

With --cameras set, teleop opens the locally attached ZED cameras by serial number and relays them to the headset over WebRTC, so the operator sees the robot’s cameras instead of (or alongside) passthrough. All streamed feeds show at once and can be moved and resized with the controllers — see the VR Interface guide for the layout and controls. Requires pyzed (zed.install) and the GStreamer NVENC stack (gst.install — the hosted installer runs it). Teleop and collect-data share one GPU-resident video path (almond_axol.video.gst_zed): the zed-gstreamer source elements (zedxonesrc mono / zedsrc stereo) grab and NVENC-encode (nvvidconv → nvv4l2h264enc) entirely on the GPU, and Python only ever sees the encoded H.264 access units — which aiortc forwards as pre-encoded packets (RTP/SRTP/ICE + SDP signaling), with no Python encode step. The headset branch is fixed at 30 fps; camera capture remains independent and may run faster for dataset recording or policy observations. Stereo ZED X cameras are auto-detected from their serial: a stereo overhead grabs both eyes on one pass and is relayed as a single side-by-side source, overhead_sbs (both eyes packed into one track — one decoder session on the headset), rendered per-lens, while a stereo wrist is relayed as a single source under its slot name (one feed, like a mono camera). When the gst stack is unavailable the path falls back to the ZED SDK grab (almond_axol.video.zed_sdk, no lerobot dependency) plus in-Python NVENC (almond_axol.video.hw_video), which streams the overhead as the two per-eye sources overhead_left / overhead_right instead. The whole relay — camera grab/encode plus WebRTC sending — runs in a dedicated subprocess (almond_axol.video.video_proc), the same isolation pattern used for the IK solver: it keeps the camera pump off the teleop process so the control loops stay at full rate. Only SDP signaling crosses the process boundary. For low-latency encode the NVENC/VIC engine clocks must be pinned to their maximum (the default Jetson devfreq governor scales them for throughput, not latency, which roughly triples per-frame encode time). This is handled by axol provision (see jetson.setup) and re-applied at boot by the installer’s systemd unit — not at teleop startup. If no GStreamer hardware encoder is present at all, aiortc falls back to software H.264 — SVGA is then the recommended resolution.

Jelly

Robots fitted with Jelly (x-drive omni base + telescoping lift) let the operator drive the whole robot from the headset thumbsticks — left stick translates, right stick rotates, the stick clicks run the lift. Nothing needs enabling: Jelly is inferred from the CAN devices attached — the wheels are driven when their interface (can_alm_axol_b) exists, the lift when its bus does (can_alm_axol_c, or the shared wheel bus). can.setup and the control panel’s automatic CAN discovery pin those names. --jelly.wheels false / --jelly.lift false (the control panel’s Jelly wheels / Jelly lift switches) keep attached hardware cold; the other parameters live in the nested jelly config (--jelly.max_speed, --jelly.channel, --jelly.imu, …). Jelly motion is operator mobility only — it’s never recorded into a dataset and a policy never drives it. --arms false runs just Jelly: the arms are never constructed and the Axol hub CAN bus is never opened, so you can drive Jelly without the arm hardware connected. The same happens on its own when the arm hub’s interfaces are absent but Jelly’s are present (a warning says so). Jelly brings its own CAN interface up on start, so no separate can.enable step is needed. For the full control mapping see the VR interface guide.
This command takes its config via draccus, not fixed argparse flags. Field names use underscores, nested fields are dot-separated, None is null, and lists use [a,b,c] — see Command configuration for the full model.