web/ (it was previously the separate axol-vr repo). The same web/ build also produces the web control panel, and axol serve serves both.
Hosted vs. self-hosted
Either way, the headset connects to the VR WebSocket server on port 8000 that
axol teleop (and the control panel’s teleop/collect operations) runs on the host.
On networks where Wi-Fi adds latency, tick Quest over USB on the connect screen to stream controller poses over a USB cable instead — the camera feeds keep using the LAN. See Quest over USB.
Repository layout
Build & develop
Fromweb/:
Deployment
The hosted app is built on Vercel;vercel.json builds the client package first so it’s available as a local workspace dependency:
Camera views
When the host has camera video enabled (axol teleop --cameras, collect-data, or the control panel with camera slots assigned), the robot’s camera feeds stream to the headset over WebRTC and render as screens over passthrough.
All streamed feeds are shown at once — the overhead centered (rendered per-eye in true stereo when the overhead is a stereo ZED X) with the left and right wrist cams as bottom-corner picture-in-picture panes. There’s no view to switch between; you arrange the screens to taste instead.
The screens are world-anchored — like TVs placed where you were looking when the session started, so you can move your head freely while they stay put. While arm tracking is disengaged you can rearrange them:
- Move a screen — point a controller at it and hold the trigger, then move the controller to reposition it. Each screen remembers its own spot.
- Resize a screen — grab the same screen with both triggers and move your hands apart to make it bigger or together to make it smaller.
- Reset — press B (right controller) to re-anchor every screen to your current gaze and clear all moves and resizes.
Controller bindings

Engaging and freezing arms. Tracking always starts with both grips together from rest. Once engaged, the grips act per arm: a click freezes only that arm — it holds its pose and keeps its gripper where it is, so it can steady a grasped object while the other arm keeps working — and another click resumes it. A forced disengage (a reset, a contact stop, or a dropped link) drops both arms and needs a deliberate both-grips re-engage. Prefer a dead-man feel? Turn on Hold grips to engage (the
hold_to_engage setting, or --teleop.hold_to_engage) and each arm tracks only while you hold its grip.The session’s mode is fixed by the command that launched it — there’s no in-headset toggle between the two.
axol teleop runs plain teleop (recording is unavailable, so A does nothing and the HUD stays on Teleop), while axol collect-data runs data collection (recording enabled, HUD starts on Data Collection).A dropped link never moves the robot. If the pose stream stops while tracking is engaged — you doff the headset, open the system menu, quit the app, or the link drops — the arms auto-disengage after about half a second and hold position wherever they are. When you return, re-engaging (both grips) takes a fresh snapshot, so the arms never jerk toward the controllers on the next entry; press X to send them back to the rest pose when you’re ready.
Powered cart
Robots equipped with the powered Axol Cart (an x-drive omni-wheel base plus a telescoping lift) are driven from the headset thumbsticks, so an operator can reposition the whole robot without leaving VR:- Left thumbstick — drive the base: push to move forward/back and left/right (strafe).
- Right thumbstick (x-axis) — rotate the base.
- Stick clicks — press the left stick in to lower the lift, the right stick to raise it (each while held).
axol teleop --cart.enabled true; the tunables live under Advanced → Cart (or the --cart.* flags). To drive only the cart with the arms left untouched — no Axol hub needed — use the control panel’s Cart only checkbox or axol teleop --cart_only. See can.setup for wiring the cart’s own CAN adapter.
The telescoping lift needs a one-time axol lift.home calibration before it will move, and axol lift.goto raises it to the robot-install height; both are also on the panel’s diagnostics dashboard under Diagnostics.
State machine
In teleop mode the headset stays inTeleop with the recording controls inert. In data collection mode it starts in Data Collection and drives episodes with A / X:
DataCollection; it becomes Recording once the countdown completes.
Stopping a recording is confirmation-gated: the first A (save) or X (discard) press while recording arms an in-headset Save episode? / Discard episode? popup instead of stopping immediately — pressing the same button confirms, the other cancels and keeps recording. Nothing is committed server-side until you confirm a save; confirming a discard drops the take (via the reset flag) so you can re-record.
During data collection the HUD also shows an Episode N readout (top-right, under the recording status) so you can see which episode you’re about to record. The server announces it on connect — a headset joining mid-session shows the right number immediately — and it’s hidden in plain teleop. See the VR server API for the
episode feedback message.{"type": "state", "value": "saving"} and blocks every control except Y (exit) until the episode is written, then pushes data_collection to re-enable controls. Broadcasting error shows an error indicator in the headset. See almond_axol.vr for the frame protocol and send_feedback_state().
Next steps
Teleoperation
Go from install to a live VR session.
VR server API
The WSS server, frame schema, and feedback messages.
