Skip to main content
Connects a VR headset to the robot (or simulator) for teleoperation. IK runs in a dedicated subprocess to keep JAX off the asyncio event loop. The pipeline is: VR frames → lag-compensated low-pass filtering → IK solve → EMA smoothing → trapezoidal velocity profiling → motion_control().
Each config left None is built from the robot’s shared settings file (~/.almond/settings.json — the one the control panel edits and axol teleop reads) over the built-in defaults, so an SDK session runs the same rest poses, multipliers, IK weights and VR port as the panel. Pass a config explicitly to override it; see Shared settings. Use step() instead of run() to integrate teleoperation into your own control loop:
To show camera feeds in the headset, register frame sources after the session is enabled — VRTeleop.set_video_sources() forwards them to the VR server’s WebRTC relay (requires the GStreamer NVENC stack, installed by axol gst.install; see almond_axol.vr). Each value is a connected ZedCamera (registered directly) or any object exposing width / height / fps + wait_next:
See the teleop CLI command for the equivalent command-line entry point (--cameras wires the ZED cameras up automatically).
set_video_sources() runs the WebRTC relay in-process. The CLI instead prefers VRTeleop.set_video_manager(), which hands the camera grab/encode and WebRTC off to a dedicated subprocess (almond_axol.video.video_proc.VideoRelayProcess) so it can’t perturb the control loops. Both deliver the same headset video — see almond_axol.vr.

VRTeleopConfig fields

Engage behaviour. From rest, both grip buttons together engage both arms. Once engaged, each grip acts per arm — a click toggles that arm between tracking and frozen (a frozen arm holds its pose and grasp while the other keeps tracking). Set hold_to_engage for a dead-man scheme instead: hold both grips to start, release one to freeze that arm, hold on to keep it going. A rising edge on the reset button triggers a trajectory back to the rest pose (collision-aware when KinematicsConfig.self_collision is on; joint-space with limits otherwise). In box mode the pair engages on the switch itself, led by box_lead_hand; from a frozen pair a single grip engages both arms again, blending them into the parallel pair first, and the pressed controller leads.Re-engaging. When a grip re-engages an arm (after a freeze, a disengage, or the arm being moved by hand), reengage decides who moves: with "clutch" nothing does — the controller’s current pose becomes the new origin — while with "ramp" the IK worker keeps the controller↔arm mapping from the arm’s previous engage and blends the arm’s target from its current pose to where that mapping places the controller (smoothstep, paced by reengage_ramp_speed with a reengage_ramp_min_s floor; the goal is live, so a moving hand is followed). The engage-edge frame itself never moves the arm in either mode. The mapping is dropped by IKWorker.reset() / clear_engage() (return to rest, DAgger sync), so the first engage after those snaps fresh regardless of mode.Live settings. box_mode, reengage, hold_to_engage, position_multiplier and teleop_max_vel (plus the grippers’ torque limit on hardware) can change while a session runs — from the headset HUD or the control panel — through almond_axol.teleop.live.LiveSettings, which validates a request and hands it to VRTeleopCore.set_live(key, value). Requests are queued and applied on the IK thread before the next frame (or while idle), and fields the IK worker reads are mirrored to it as ("set", key, value) messages, so the two processes’ copies of the config never disagree mid-step. See the VR server API for the wire protocol.