axol tracker.* commands are the CLI side of Mantis Tracking: the control panel’s Mantis settings → Tracking flow runs the same commands on the host. This page is the flag and file-format reference; the setup walkthrough for each source lives on the tracking page.
Direct
axol collect-data --mantis true runs inherit the source, channel map, bindings, and (for Quest) calibration key saved by the panel, own their bridge, and enforce the same readiness gates. Do not launch a second tracker.bridge beside a managed run — it stays view-only and cannot win pose control. (axol teleop --mantis never tracks: it drives the grippers from the rig triggers over CAN and starts no bridge.)
axol tracker.bridge
Streams a hardware tracker backend’s poses (and each side’s trigger value) to the VR server in the standard VRFrame format.
With the
survive backend, an unexpected survive-cli exit (a crash) no longer ends the bridge: it is restarted after about a second, and since its calibration is saved both trackers report again a few seconds later; the rigs freeze meanwhile as for any tracking loss. More than 3 exits within a minute is treated as a crash loop and fails the source, and the exit report names the signal and the last lines of survive-cli’s stderr. libsurvive only enumerates USB devices at startup, so when a tracker drops off the bus (or a fresh process leaves one silent) the bridge restarts survive-cli and re-checks every 5 seconds, for up to 60 seconds, until every tracker seen earlier reports again.
A side whose tracker loses tracking (occlusion, SLAM relocalising) holds its last good pose and logs a warning rather than feeding IK a glitch. A stale trigger node (no frames for ~250 ms) likewise holds the last grip command and logs the dropout. A managed bridge freezes motion, waits for both trackers to become fresh, then requires the two-trigger start gesture before it re-anchors; the standalone bridge keeps its manual controls: Enter toggles engage, r returns to rest, q quits. A standalone bridge warns and continues fully open when a configured trigger is absent; a managed production run fails readiness instead.
Hardware-free protocol demo
To exercise the absolute-mapping protocol without a Mantis source, run a generic sim server and a standalone static bridge:static over synthetic when isolating engage/reset plumbing. This proves only the JSON/IK/input path — it selects no Mantis source, resolves no tracker→TCP transform, and is not acceptance. Both hardware-free backends bind their own devices and need no tracker.identify.
axol tracker.identify
Discovers the powered-on trackers, then asks you to move each rig in turn; the device with the most motion is bound to that side. For Lighthouse it runs the same channel check as tracker.lighthouse.check after discovery and refuses to bind while a clash is present. Bindings are kept separately per family so the panel can switch between Lighthouse and Ultimate.
Saves
~/.almond/tracker/config.json:
WM0, WM1) number the Watchman dongles in the order they are opened and can swap between runs, so releases before 0.2.25, which saved those names, could bind each rig to the other’s tracker. A saved binding that is still a libsurvive name is cleared on load; run Identify trackers once to bind by serial.
axol tracker.pair
Pairs one VIVE Tracker 3.0 with its Watchman dongle through survive-cli --pair-device 1 (which needs the libusb backend tracker.install builds). The tracker must be unplugged from USB and blinking blue. Run it once per tracker/dongle pair.
libsurvive only opens pairing mode on a dongle that has no tracker connected. If every connected dongle already brings up a powered tracker, the command says they are already paired instead of waiting; to pair a different tracker to a dongle, switch off the tracker currently paired to it first.
axol tracker.install
Installs the tested libsurvive commit f1e6eddb669320f2a30760f4b42936bdb4306da0 with seven fixes applied on top: build dependencies, survive-cli built with Eigen and the native Linux libusb backend (not OpenBLAS or HIDAPI), and the Vive udev rule. The fixes ship in the package (almond_axol/tracker/libsurvive_patches/; the timestamp, base-station, scene-solver and solver-stack fixes carry a regression test in libsurvive’s own suite):
- Timestamps across the tracker clock rollover. The tracker’s 48 MHz clock wraps every ~89 s; a laser measurement taken just before the wrap was stamped ~350 ms in the future, which could stall pose output while tracker data kept arriving.
- Solver buffer overflow with more base stations. Upstream libsurvive’s fix (
df22f2d, backported verbatim): the MPFIT poser copied the solver’s covariance into a fixed 28×28 buffer, which overflows once the tracker pose, per-station corrections and stations being re-solved exceed 28 parameters. That is reachable with four base stations and can crashsurvive-cli. - Scene-solver crash. libsurvive’s global scene solver keeps 32 calibration scenes; once a long session filled that ring, the loops after each solve read one past its end and crashed
survive-cli(SIGSEGV), ending the session. Setups with more base stations fill it sooner. Those loops are now bounded, and scenes of a disconnected tracker are compacted without mixing up the remaining scenes’ measurements. - Solver stack overflow. The optimizer (mpfit) put its work arrays, including the Jacobian, on the stack. A global solve over a full scene ring with four base stations needs several megabytes for it, which overflowed the solver thread’s stack and crashed
survive-cli. They are now heap-allocated. - Smaller fixes. A data race on the scene solver’s “solve needed” flag, a mis-indexed measurement count in the optional per-station correction setup (off by default), and a read past the end of an empty config file’s parse.
- Poses delivered as they are produced.
survive-cli --record-stdoutnever flushed its output, so through a pipe the poses arrived in 4 KB blocks: about 50 ms late at the median and up to about 110 ms with two trackers. The stream is now line-buffered. - Base-station identity through every tracker. Each base station’s OOTX broadcast (its identity and calibration) was decoded only through the first tracker that saw it; if that tracker received it poorly, the other tracker’s clean copy was ignored and calibration could stall. Every tracker now decodes it, and the first valid packet wins.
axol provision runs it automatically; rerun it directly to repair. When the machine-wide install proof under /var/lib/almond/ is missing or in an older format but the local build cache already holds the pinned build with the same options (for example after updating Axol), the command only refreshes the udev rule and re-attests the installed files instead of rebuilding. The proof is machine-wide, so root provisioning/serve and an operator CLI agree even though their writable build caches differ.
On ARM, avoid linking libsurvive against OpenBLAS (known instability/CPU blowups);
-DUSE_EIGEN=ON sidesteps it. Close SteamVR if one is somehow present — it competes for the devices.axol tracker.lighthouse.check
~/.almond/tracker/lighthouse_survey.json and gates the Trackers step of the panel flow.
Every Axol survive-cli run (tracking, the check, pairing) uses libsurvive’s calibration file ~/.config/libsurvive/config.json (under $XDG_CONFIG_HOME when set) of the account it runs as; the root service uses /root/.config/libsurvive/config.json. Releases before 0.2.25 let the service, which runs without HOME, write it to /config.json; the first run adopts that file so existing base-station positions are kept.
axol tracker.ultimate.install
fcfcd33f4c1f16b0d84f5f741dc1319abdc7942a, the hid>=1.0.5 Python binding (not the unrelated hidapi package), system HID libraries, and a persistent udev rule for dongle 0bb4:0350. Safe to rerun; do not replace it with an unpinned uv pip install or a one-off chmod /dev/hidraw*. The Windows pairing and mapping steps that must precede it are on the tracking page.
Ultimate Wi-Fi file
ultimate_wifi.json is the private shared-map AP configuration the panel’s Wi-Fi step writes. For a CLI/manual setup, create ~/.almond/tracker/ultimate_wifi.json (or $ALMOND_HOME/tracker/ultimate_wifi.json) with this shape and chmod 600 it:
country is consumed directly, so the SSID/password/frequency are fallback configuration, not a promise of the active broadcast network. Choose a country and frequency valid for the workspace. The readiness check reports only validity, never credential values.
axol tracker.ultimate.check
tracker.identify again when you need to prove live poses.
Transform file
~/.almond/mantis/tcp_transform.json holds measured tracker→TCP transforms, keyed per side by the identified device. The built-in factory values for the standard Tracker 3.0 and Ultimate flat-back mounts apply automatically when no conflicting entry exists; a saved entry is a higher-priority override. Quest 3 Touch Plus controllers (quest:meta-quest-touch-plus:grip) have a factory value too, used only when no Quest entry is saved; other Quest controllers have none and need an entry for both sides.
~/.almond/tracker/config.json (or the Quest key the panel reports) and the measured values; the identity values above are schema placeholders, not a usable calibration. pos is the gripper TCP origin in the tracker’s local frame in metres; quat is the TCP orientation in that frame as [qx, qy, qz, qw], normalized on load. The file can also be written through PUT /api/tracker/calibration/<source>; the control panel does not edit it.
Rules the loader enforces:
- Quest production collection accepts only a
quest:<reported-profile>:gripentry on both sides, or the built-in Quest 3 factory value forquest:meta-quest-touch-plus:grip. A barequestkey, a different Touch profile, missing profile metadata, or atarget-rayentry fails closed. A saved Quest entry always takes precedence; the factory key is selected only when none is saved. - Ultimate entries must carry
ultimate_pose_conventionmatching both the convention used during the measurement and the activeultimate_quat_order/ultimate_up_axis. A convention-less or mismatched entry is not authoritative and suppresses the factory fallback for that side until fixed or removed, because it may represent a non-standard mount. - An old unkeyed entry, a bare
survive/ultimatekey, or a same-family entry left behind after rebinding also suppresses the factory value for that side until it is re-keyed or deleted. - A standalone Advanced/CLI transform can support a bring-up capture, but production
collect-datarequires an active device-keyed override or an approved factory value on both sides;--mantis_allow_uncalibrated trueis only for a calibration capture that will not be used for training.
Vive factory values
Both Vive families sit flat on top of the rig, behind the gripper motor, and share the mount rotationRy(180°), stored as the quaternion [0, 1, 0, 0]: the bridge-reported tracker +z lies along the gripper’s finger direction (gripper −z) and the tracker’s +x along the gripper’s −x, with the y axis shared. In the gripper frame (origin at the centre of the gripper motor’s mounting face) the Tracker 3.0 origin is at [0, −35, 47] mm, 47 mm behind the motor face and 35 mm above it, so the stored value (the gripper origin in the tracker frame) is [0, 0.035, 0.047, 0, 1, 0, 0] for both sides.
The Ultimate’s origin sits 11 mm higher on the same mount, at [0, −46, 47] mm in the gripper frame, so its value is [0, 0.046, 0.047, 0, 1, 0, 0] for both sides; the tracker-local axes and flat-back orientation match V3, so there is no additional rotation.
axol 0.2.5–0.2.16 shipped [0, 0.0355, -0.092] / [0, 0.0465, -0.092] as the translation, which put the tracker 92 mm in front of the gripper instead of 47 mm behind it. Those datasets record vive-flat-back-ry180-v0.2.5 as their mantis_tcp_transform id, and collect-data refuses to append to them under the current constants.
Quest factory value
Quest 3 Touch Plus controllers (quest:meta-quest-touch-plus:grip) in the standard cradles have their WebXR grip origin at [0, −45, 62] mm in the gripper frame, 62 mm behind and 45 mm above the motor face, with the grip axes rotated 180° about the gripper’s z axis plus a ~10° cradle tilt. The left cradle is the right one’s mirror image, so the tilt mirrors between sides:
Other controller generations still need a measured entry.
Deriving a missing constant
If a source transform is missing — a Quest controller other than the Quest 3 Touch Plus, or an Ultimate run under a non-factory parser convention — first pass the acceptance checks for side, scale, and recovery, then run a short capture with--mantis_allow_uncalibrated true, inspect it to derive the constant, install it under the exact source/device key, and repeat the loss/recovery check. Normal collect-data --mantis stays blocked until both transforms resolve; never mix an uncalibrated capture into training data.