Skip to main content
Installs and refreshes the pieces uv tool install can’t manage on its own:
  • adb + the Quest udev rule — for the Quest-over-USB controller transport (the adb reverse tunnel that streams poses over the cable). Self-gates on apt.
  • tracker.install — a pinned libsurvive build and Vive USB permissions for Mantis Lighthouse tracking.
  • zed.driver — replaces a ZED Box’s outdated factory GMSL camera driver (stereolabs-zedbox-duo on the Duo, stereolabs-zedbox-mini on the Mini, stereolabs-zedlink-duo for a ZED Link Duo card on a Jetson AGX Orin) with the release pinned for the ZED SDK. Takes effect at boot, so provisioning ends in a reboot (see below). A no-op on a host with no Stereolabs driver package installed; warns about a Stereolabs driver package it has no pin for.
  • board IMU (gyro.install) — grants the imu group access to the carrier board’s BMI088 sampling timer, Jelly’s heading-hold yaw reference, so teleop can start it without root. Self-gates on the board driver’s presence.
  • CAN e-stop purge grant — writes /etc/sudoers.d/50-axol-can-purge so a manual axol serve / axol teleop can flap a CAN interface without a password when motor power dies. The realtime core purges the interface’s queued position commands the moment it declares the bus stalled; without this grant that escalation fails on a non-root session, the frames survive, and the arm jerks to its pre-e-stop command on the next bring-up (see can.setup). The rule covers only the root-owned reset and bring-up scripts (the arm hub is USB-reset, since its firmware keeps frames a flap cannot reach) and the named CAN interfaces, and is checked with visudo before it is installed. Takes effect immediately.
  • real-time scheduling grant — writes /etc/security/limits.d/50-axol-rtprio.conf so your login can run the control loop and the camera relay’s capture chain SCHED_FIFO from a manual axol serve / axol teleop (the systemd unit already has LimitRTPRIO). Login shells otherwise start with an rtprio limit of zero, and then:
    • the control loop refuses to start — axol teleop, collect-data, collect-dagger and run-policy raise refusing to run without real-time scheduling rather than run a loop that lands roughly one tick in fifty 15–65 ms late (felt as the arms hitching and lunging, and masked by turning the cameras off). Set AXOL_ALLOW_CFS_CONTROL=1 to run degraded anyway.
    • the relay silently falls back to CFS — camera capture threads stay SCHED_OTHER at startup, and recordings under load discard episodes on skipped exposures.
    Takes effect at your next login; check with ulimit -r (should print 20).
    Only PAM applies this file, so it reaches a normal ssh login and nothing else. A session that bypasses PAM — Tailscale SSH (and terminals tunnelled over it), or a systemd unit without LimitRTPRIO — inherits its parent’s limit of zero no matter how many times you run axol provision. provision fixes this for you: when the session it runs in can’t use the grant, it raises the limit in place (prlimit, with the sudo it already holds) on every ancestor process you own — your shell and the terminal host above it (an IDE’s node, tmux), so later commands from that shell and new terminals from that host inherit it. It stops at processes owned by another account (sshd, tailscaled), which it leaves alone, and the fix lasts as long as those processes do; a fresh ssh login gets it from the grant. Only when it can’t escalate does it fall back to naming the ancestor the limit came from and both manual ways out: reconnect over ssh, or run sudo prlimit --pid $$ --rtprio=20:20 in that shell. The check stays silent when run as root (the curl installer, the systemd unit): root holds CAP_SYS_NICE, which bypasses RLIMIT_RTPRIO entirely, so root’s own limit of zero is normal and harmless. Note axol-rt is unaffected either way — rt.install gives the binary cap_sys_nice, so the CAN loops stay real-time while the Python control loop is not.
  • zed.install — the pyzed bindings (not on PyPI; needs the ZED SDK).
  • gst.install — the GStreamer + PyGObject appsink stack (PyGObject builds against the system gobject-introspection and is dropped whenever the tool environment is rebuilt, e.g. by a release upgrade).
  • gst.build-zed — the patched zedxonesrc / zedsrc plugins (sensor-accurate PTS so collected images line up with joint samples).
  • rt.install — the required axol-rt realtime core hardware-control binary (Rust toolchain via rustup if needed; sources fetched at the installed package’s exact ref on tool installs). Self-gates on network/toolchain availability.
Every step is idempotent and best-effort by default — failures are logged while the remaining steps continue, and optional hardware steps self-gate on the ZED SDK / apt / NVENC — so plain axol provision is safe to run on any host and re-run anytime. Use the strict form when the required control core must be present:
--require-rt leaves the optional steps best-effort but makes the command exit non-zero if rt.install fails. The hosted installer and the axol serve self-updater use this form, so an RT build or capability-setup failure aborts installation or blocks the restart onto updated code. Both still share this single provisioning path, keeping their steps in lockstep. When only optional steps fail (Lighthouse tracking, the patched zed-gstreamer plugins, adb, …), the last line names them (Optional provisioning steps failed: ...) and the command exits 3. The installer then finishes with a warning instead of aborting. A provision run by axol serve exits 0 in that case, so an update still restarts onto the new release, and the control panel shows the missing steps as a warning. A failed rt.install exits 1. Every apt-get / dpkg run is protected from interruption: it never times out, and it runs in its own session and systemd scope, so a stopped or restarted axol.service or a timed-out update leaves it to finish. A package manager killed partway leaves dpkg half-configured, and every later install on the host then fails. When apt fails, the log names the package dpkg could not process. Repair it with sudo dpkg --configure -a, then re-run axol provision. Provisioning holds the host-wide update lock, which is root-only. Run from a terminal as a regular user, the steps still execute as that user (their uv, download caches, and Python environment) and only a small lock-holder process escalates via sudo (one password prompt, shared with the steps that need root). Without a usable sudo the command exits with a message instead of a traceback.
  • host tuning — last, the runtime tuning for the host it runs on, detected from what the host exposes. On a Jetson (Orin NX 16GB, AGX Orin, Thor T5000): the max power mode, the NVENC/VIC/GPU and CPU clock pins, the CAN adapters’ interrupt steered onto a CAN core, and the Argus camera daemon run SCHED_FIFO on the camera cores — see jetson.setup for why each matters. On a Raspberry Pi 5 or a workstation there is nothing to tune. Either way it logs the board and the CPU core layout the control, IK, CAN, camera and recorder work will pin to, e.g. core layout: 14 cores online: CAN 12-13, control 2, IK 3, relay 4-5, recorder 10-11, camera 0-1,5-9.
This tuning resets at every reboot, so axol provision --boot runs just this step — no installs, no update lock — and the installer’s axol.service runs it as its ExecStartPre at each boot. You never need a second command: after pulling a fix, axol provision (and logging in again for the scheduling grant) is enough. axol jetson.setup still works as an alias of --boot, for units written by older installers. The axol serve self-updater’s provisioning pass skips this step: it can overlap a robot session, and the service restart the update ends with re-tunes the host before serve starts.

Reboots

Some steps only take effect at boot: a new ZED Box camera driver, or a Jetson power mode that nvpmodel will only switch across a reboot. When one of them runs, a clean axol provision reboots the host at the end of the run, which ends an SSH session. A run with failed steps never reboots. The pending reboot is remembered until the host restarts, so the successful retry still reboots, even though the step that asked is now a no-op. The hosted installer provisions with --no-reboot and finishes with --apply-reboot, after writing the service. The axol serve self-updater provisions with --no-reboot, then reboots the host in place of its usual service restart, and only while no operation is running. A provision started by axol serve never reboots on its own, whatever its flags. If a reboot didn’t clear the step that asked for it, provision won’t reboot again for the same reason. It reports the problem instead of looping the robot through reboots. When there’s no ZED SDK at /usr/local/zed, only the pyzed and zed-gstreamer build steps are skipped. The GStreamer/NVENC steps also skip on non-Jetson hosts, where that pipeline cannot run. adb, board access, the scheduling grant, Lighthouse tracking, and axol-rt are still provisioned on normal Linux workstations. See Development install → Headset-video stack for when you’d run this by hand.