Skip to main content
The 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.)
Quest uses none of these commands: the headset’s WebXR client is the pose source, and its calibration key is selected in the panel. Mantis is not supported by Collect DAgger or Run Policy.

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. 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:
Press Enter in the bridge terminal to engage; the simulated arms remain at the static poses. Prefer 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:

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.

axol tracker.install

Installs the tested libsurvive commit f1e6eddb669320f2a30760f4b42936bdb4306da0: build dependencies, survive-cli built with Eigen and the native Linux libusb backend (not OpenBLAS or HIDAPI), and the Vive udev rule. 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.
Manual fallback from the same upstream source:
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

Listens to libsurvive for about 20 seconds with both trackers powered and in view, then reports one line per base-station channel a tracker actually received (with the station serials once decoded) and the trackers that are reporting. It fails when two stations share a channel, fewer than two base stations are seen, or fewer than two trackers report; each failure line ends with the fix. Stations replayed from libsurvive’s saved calibration are not counted as seen, so a station that was re-channelled or unplugged since the last session cannot mask the problem; if the station now on a channel is not the one saved there last time, the check says so — with two stations that means both are on that channel, since libsurvive can only lock onto one station per channel. The survey is saved to ~/.almond/tracker/lighthouse_survey.json and gates the Trackers step of the panel flow.

axol tracker.ultimate.install

Installs pyvut commit 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:
All four fields are required by upstream pyvut. With the USB-dongle transport the tracker firmware reports the active SLAM-host SSID/password/frequency and pyvut relays those to the other tracker; only 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

Non-invasive dependency, USB, permission, binding, pose-convention, and redacted AP-config check — the same state the operation Start gate uses. Missing or insecure AP config, inaccessible HID, or absent operator access makes it fail. It deliberately does not open the dongle or claim live tracking; run 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 has no factory value and needs an entry for both sides.
Replace every placeholder with the exact identifiers from ~/.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>:grip entry on both sides. A bare quest key, a different Touch profile, missing profile metadata, or a target-ray entry fails closed.
  • Ultimate entries must carry ultimate_pose_convention matching both the convention used during the measurement and the active ultimate_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 / ultimate key, 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-data requires an active device-keyed override or an approved factory value on both sides; --mantis_allow_uncalibrated true is only for a calibration capture that will not be used for training.

Ultimate factory value

The Ultimate constant derives from the Tracker 3.0 mount transform. In the shared gripper/CAD frame the V3 and Ultimate reference origins are [47, 0, 35] mm and [47, 0, 46] mm, so the tracker-origin change is ΔO = [0, 0, +11] mm. For the stored tracker→TCP transform, Δp = −R·ΔO; the inherited quaternion [0.7071068, 0, 0, 0.7071068] is R = Rx(+90°) and maps CAD +z to tracker −y, so Δp = [0, +11, 0] mm: the 35.5 mm tracker-y component becomes 46.5 mm while the −92 mm forward component is unchanged. The approved value is [0, 0.0465, -0.092, 0.7071068, 0, 0, 0.7071068] for both sides; the tracker-local axes and flat-back orientation match V3, so there is no additional rotation. This mount-frame calculation is distinct from the bridge’s world-frame z-up→y-up relabel.

Deriving a missing constant

If a source transform is missing — Quest, 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.