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. The layout is responsive, so you can run it from a phone or tablet on the same network, not just a desktop.
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); if the host can’t be reached the attempt gives up after a couple of seconds with a can’t reach the host message instead of leaving the tile stuck on Connecting…. When connected, the tile shows the host’s name, its installed release version (e.g. axol v0.1.2), and a green dot. Teleop and gravity comp need only this connection.
A connected tile carries three buttons:
- Restart reboots the robot host (
shutdown -r now) behind a confirmation dialog. The panel loses its connection while the host reboots — reconnect once it’s back up, typically under a minute. - Shut down powers the host off (
shutdown -h now), also behind a confirmation. Turning the robot back on requires physical access to it. - Disconnect (the unplug button) drops this browser only — a client-side disconnect that leaves the server, the robot link, and any running operation untouched, so another operator driving the same host isn’t affected.
axol serve escalates with sudo -n, so a host with no passwordless sudo reports that it couldn’t and stays up.)
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 when the Axol Hub adapter is used and the host hasn’t been configured yet. A non-hub SocketCAN adapter (chosen per arm in the Robot settings tab, or via the diagnostics dashboard’s CAN adapter picker) is brought up directly instead. - 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 and collect-data). Run policy relays no video, so it ignores this branch entirely.
- Record — what is written to the dataset during data collection (downscaled on the GPU’s VIC, independently of the stream). Because run policy streams no headset video, it also captures its policy observations at this resolution — the size the dataset was recorded at and the policy was trained on, rather than grabbing a larger headset frame it would only throw away.
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.
The tab also carries the headless Keep awake / Restore buttons: with the headset plugged in and authorized, Keep awake disables its proximity sensor over adb so the VR session keeps running with the headset set down, and Restore brings normal sleep behavior back (a headset reboot also resets it).
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. While you’re scrolled up, a floating arrow button in the lower-right of the log area jumps you back to the latest line and re-engages auto-follow. 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. Robot Disconnect likewise stops the running op and waits for it to finish tearing down before releasing the robot link; the host Disconnect, by contrast, is a client-side drop that leaves any running op in place server-side.
Operator view
While teleoperation or collect data streams camera video to the headset (camera slots assigned), the control panel mirrors those live camera feeds, so you can watch the robot’s point of view on the dashboard and not only in VR. The panel joins the running operation’s video stream as one more view-only client and shows the same tracks the headset receives — no extra work for the robot, and the Cameras settings tab still decides what streams. A side-by-side stereo overhead is shown cropped to its left eye. (A sim or Cart only teleop run shows no mirrored feeds — neither relays headset video.) The fullscreen toggle on the camera-feeds header expands the feeds to a full-screen operator display that fits without scrolling; Esc or Exit fullscreen leaves. If the feeds never connect, the panel offers a one-tap button to authorize the stream server’s self-signed certificate — the same one-time step the VR app uses.Episode controls
While Run policy or Collect data is live, an Episode control box appears in the operation card, led by a colored phase badge (Ready, Recording, Saving, Resetting, …) with the episode number and saved count. The box is driven by the host, so it shows the same phase on every connected computer, not just the tab that started the run. To end the run entirely, use Stop in the card header. For Run policy:
These mirror the
s / r keystrokes and the Enter prompts the run-policy CLI reads from stdin.
For Collect data, the buttons do exactly what the VR controller does, so a session can run with the headset off — pair them with the mirrored camera feeds above:
The VR controller stays live throughout — the panel is a parallel input, not a replacement, and whichever arrives first ends the take. Anything the operator arms on the controller (the Save episode? / Discard episode? confirmation and the record countdown) is mirrored into the panel as a popup, so a headset worn off the face is still followable.
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.
