axol serve and runs each operation in-process inside that server, so it’s a drop-in alternative to the per-command CLI workflow.
Start the server
With the one-command install, the server is already running on the robot machine (the computer wired to the robot, with the ZED cameras attached) as a root systemd service that starts at boot and restarts automatically — open axol.almond.bot and connect to the host. On a development install, start it yourself:/control.
For a development install, the local UI must be built once (
cd web && npm install, then npm run build for the axol-vr-client and app workspaces) — or skip the build and use the hosted UI at axol.almond.bot, which connects to your axol serve directly. See Development install.Connections
The top of the panel is a pair of tiles — Axol Host and Axol — each with a colored status dot. (Cameras and the Quest USB link are configured in the Settings tabs right below.)Axol Host
The Axol Host is the machine runningaxol serve — the computer wired to the robot, with the ZED cameras attached. Press Connect on the tile to open the host dialog and enter its address (just the IP — port 8001 is assumed). When connected, the tile shows the host’s name, its installed release version (e.g. axol v0.1.2), and a green dot; press the power button to disconnect. Teleop and gravity comp need only this connection.
Updates
When a newer release than the installed one exists, a banner appears above the tiles showing the version jump (e.g.v0.1.2 → v0.1.3) with a link to the release notes. Press Update to apply it: the host reinstalls at the new release, re-provisions its system dependencies, and restarts once idle — the button is disabled while an operation is running, since applying an update restarts the server. Nothing updates without that button press.
The same banner area also warns when the control panel UI and the host backend are running different code — the web bundle bakes in the git commit and release version it was built from, the host reports its own via /api/info, and a mismatch raises the warning (a released host is compared by version, a dev checkout by commit). This catches a stale hosted tab, or an unbuilt local web/app/dist, silently driving a newer or older backend; clear it by updating the host to the matching release or rebuilding the local UI.
Axol
The panel connects the robot automatically once the host comes online (one attempt per host session, so a manual disconnect isn’t immediately undone); the Connect button is the manual fallback / reconnect. Bringing up the robot:- Enables the CAN interfaces, running
can.setupautomatically the first time if the host hasn’t been configured yet. - Pings all 16 motors once per second while idle, so the tile reflects live motor health as two rows of squares (grouped L / R): the top row is CAN reachability, the bottom row the motor’s error status. Hover any square for the details (status, temperature).
Settings
Almost nothing you configure is actually per-run — camera assignment, arm stiffness, teleop rate, recording fps and codec, the inference server address — so it all lives in one Settings card, tabbed directly on the page below the connection tiles:
Values are stored on the Axol host (
~/.almond/settings.json) — not in your browser — so they’re shared by every operation and every operator device, and they’re folded into each operation start automatically (a per-run field always wins over a shared one). Edits stage locally until you press Save (or Discard). Unset fields show the config’s own default as a placeholder; edited ones get a reset button. Export / Import in the card footer move the whole settings file as JSON for backup or copying between robots.
Cameras
Data collection and policy inference also need the ZED cameras attached to the host — they’re assigned in the Cameras settings tab. The Cameras tab detects the cameras plugged into the host and shows one card per camera: its serial, its type (stereo ZED X / mono ZED-X One — detected automatically, never flagged manually), the model, and a live preview frame, so you can tell at a glance which physical camera each serial belongs to. Assign each one to its slot (overhead / left arm / right arm) — an assigned slot repeats the camera’s preview thumbnail next to its name. Press Refresh to re-query the daemon’s device list and grab fresh preview frames, or Restart daemon to restart the ZED X daemon so it re-scans the GMSL links — needed to pick up a camera plugged in or unplugged since boot. (Previews are unavailable while an operation is running, since it owns the cameras.) The Cameras tab is the single source of truth for all camera settings — these fields appear nowhere else. It controls two independent branches:- Stream — what is relayed to the VR headset over WebRTC (used by teleop, collect-data, and run-policy preview).
- Record — what is written to the dataset during data collection (downscaled on the GPU’s VIC, independently of the stream).
SVGA / HD1080 / HD1200, or Off to disable that branch globally) — so the headset can see a high-resolution feed while the dataset stores a lighter SVGA one, or vice versa. Each assigned camera then has independent Stream and Record controls: a single on/off for a mono camera, or per-eye L / R checkboxes for a stereo one (none checked = off for that branch). So you can stream a camera without recording it (or the reverse), choose which mono cameras feed the headset vs. the dataset, stream both eyes of a stereo camera for depth while recording only one, or turn either branch off entirely.
A stereo overhead captures both eyes from one grab; with both eyes selected it’s rendered per-lens in the headset (streamed as one side-by-side feed) and recorded as the two observation keys overhead_left / overhead_right. A single eye — or a stereo wrist — is exposed under the plain slot name, so it costs and reads exactly like a mono camera.
The assignments and stream/record selection persist on the host with the rest of the shared settings, so every operation and every operator device sees the same camera setup. Teleop streams whichever cameras have streaming enabled; collect data and run policy need at least one camera with recording enabled (they error early otherwise, since a policy needs the cameras it was trained on). Starting collect data or run policy also re-verifies that every assigned camera is physically present and blocks the start with an alert naming the missing serial if one isn’t — reconnect it, then Refresh (or Restart daemon) in the Cameras tab. Teleop streams whatever is configured and is never blocked by this check.
Quest USB
The Quest USB tab manages the optional Quest-over-USB link, which streams controller poses over a USB cable instead of Wi-Fi for lower latency. When a headset is plugged into the host and authorized (accept the Allow USB debugging? prompt the first time), the host sets up theadb reverse tunnel automatically and turns green (Controller over USB); the Connect button is a manual fallback / reconnect. Camera video keeps using the LAN — USB carries poses only. The headset sends every pose over both the cable and Wi-Fi, so a dropped cable fails over to Wi-Fi instantly with no reconnect, and the cable reclaims the link when it returns.
Operations
Pick an operation from the selector. Each panel asks only for its per-run inputs — the dataset repo id and task for collect data, the policy path/type for run policy, the episode to replay — plus Start/Stop. Everything reusable lives in Settings. Hover the ⓘ next to a field for its description, pulled from the CLI’s own help.Before you can start
The Start button stays disabled until everything an operation needs is ready, and a “Before you can start” list spells out what’s missing — e.g. Connect Axol, Fix motor fault: R elbow — over temperature, Assign at least one camera serial in the Cameras settings tab, or Set <required field>. Per-run values (repo ids, task names, …) are remembered by your browser so they reappear on reload; everything else persists on the host with the shared settings.Running, logs, and stopping
Starting an operation streams its logs into the console at the bottom of the page and shows a status badge. The console captures not just the operation’s Python output but also native-library and subprocess output — the ZED SDK, GStreamer, and CUDA, plus the spawned video-relay and recorder processes — so a camera or ZED failure surfaces its actual reason in the UI instead of a bare “exited”. The console auto-follows new output only while you’re scrolled to the bottom; scroll up to read earlier lines and it stops yanking you back down until you return to the bottom. The download button in the Logs header saves everything currently in the console to a timestamped.log file (disabled when there’s no output) — handy for attaching to a bug report. Press Stop to end it — the request returns immediately and the badge and button flip to Stopping… while a background watchdog tears the op down (force-killing its child subprocesses after a short grace if a save or startup is still blocking). The panel polls until the op is truly gone, then the robot link returns to idle 1 Hz polling. Host / Robot Disconnect likewise wait for the running op to finish tearing down before proceeding.
Run Policy episode controls
While Run policy is live, an Episode control box appears in the operation card with three buttons:
These mirror the
s / r keystrokes the run-policy CLI reads from stdin. To end the run entirely, use Stop in the card header.
Collect data has no on-screen episode buttons — recording is driven from the VR controller (press A to record/save, X to discard). The session is locked to data collection; there’s no mode toggle. See the Data Collection guide for the controller layout.
Next steps
Diagnostics dashboard
Live motor telemetry, health tiles, and diagnostics scripts — the sibling
/diagnostics page.serve reference
Flags, TLS, and what the server exposes.
VR interface
The in-repo WebXR app that drives teleop and recording.
