> ## Documentation Index
> Fetch the complete documentation index at: https://docs.almond.bot/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Collection

> Record VR teleoperation episodes to a LeRobot dataset — from the web control panel or the CLI.

Data collection records teleoperation episodes — joint trajectories plus synchronized camera frames — into a [LeRobot](/api/lerobot) dataset you can later train on. Everything runs on **one machine**: the computer wired to the robot with the ZED cameras attached (typically a ZED Box / Jetson). Because it owns the dataset, runs teleop, and captures the cameras locally, every recorded frame and joint sample shares the same clock — no time sync to set up.

You drive the arms over VR exactly as in [teleoperation](/operations/teleop); the session runs in **data collection** mode, where the **A** (record) controller button starts and saves takes. You can launch it from the **web control panel** or the **CLI** (`axol collect-data`).

## Before you start

* **Axol installed** ([one-command install](/installation)), CAN up, and motors verified.
* **At least one ZED camera connected** over GMSL (up to three: overhead / left arm / right arm), with their serial numbers on hand. (Camera serials are printed on the camera body, or read them from the Cameras settings tab / Python — see the tip below.)
* **The VR TLS certificate authorized** once — see [Teleoperation](/operations/teleop#authorize-the-tls-certificate).

<Tip>
  List the connected cameras and their serials from Python:

  ```bash theme={null}
  python -c "from almond_axol.zed import list_zed_devices; print(list_zed_devices())"
  ```

  If a camera was plugged in after boot and doesn't show up, restart the ZED X daemon (`sudo systemctl restart zed_x_daemon`), or press **Restart daemon** in the control panel's Cameras settings tab.
</Tip>

## Run it

<Tabs>
  <Tab title="Control Panel">
    <Steps>
      <Step title="Connect the robot and assign cameras">
        Connect the **Axol Host** and **Axol**, then open the **Cameras** settings tab and assign each detected camera to its slot (overhead / left arm / right arm) — at least one is required. Stereo ZED X cameras are detected automatically. See [Cameras](/guides/control-panel#cameras).
      </Step>

      <Step title="Select Collect Data and fill the fields">
        Pick **Collect Data**. Set the dataset **repo id** — type a new one (e.g. `myorg/pick-place`) or pick an existing dataset from the dropdown (each shown with its episode count) to **resume** it — and the **task** description; those are the only per-run inputs. Everything reusable (fps, teleop rate, stiffness, push-to-hub) lives in the [Settings](/guides/control-panel#settings) tabs.
      </Step>

      <Step title="Start and record from the headset">
        Press **Start**, connect the headset, and drive the arms. Recording is normally controlled from the VR controllers — see [Recording an episode](#recording-an-episode) below — and the panel mirrors the camera feeds and episode state while you go, with on-screen buttons for the same decisions ([Driving from the panel](#driving-from-the-panel)).
      </Step>
    </Steps>

    <Note>
      The session is locked to data collection; there's no mode toggle.
    </Note>
  </Tab>

  <Tab title="CLI">
    On the robot machine:

    ```bash theme={null}
    axol collect-data \
        --repo_id myorg/pick-place \
        --task "Pick the red cube and place it in the bin" \
        --robot_config.cameras "{overhead: {serial: 41234567}, left_arm: {serial: 41234568}, right_arm: {serial: 41234569}}"
    ```

    Replace the serials with your cameras'. Assign only the slots you have — at least one is required, and any you omit are simply not recorded (e.g. `--robot_config.cameras "{overhead: {serial: 41234567}}"` records the overhead alone). The `cameras` dict is one inline YAML value (see [Command configuration](/cli/configuration#field-name-conventions)). Stereo ZED X cameras are detected from their serial automatically: a stereo **overhead** records both eyes as `overhead_left` / `overhead_right`, while a stereo **wrist** records its left eye under the plain slot name (like a mono camera). To override the auto-detection, set `stereo: true` together with the eye policy you want — `eyes: both` records `X_left` / `X_right`, `eyes: left` (or `right`) records a single eye under the plain name `X` (so a forced wrist needs `eyes: left` to stay single-eye). For example, to force the overhead:

    ```bash theme={null}
        --robot_config.cameras "{overhead: {serial: 41234567, stereo: true}, left_arm: {serial: 41234568}, right_arm: {serial: 41234569}}"
    ```

    Connect the headset and record from the controllers — see [Recording an episode](#recording-an-episode). Press `Ctrl+C` to finish. See [`collect-data`](/cli/collect-data) for the full field list (`--fps`, `--teleop_hz`, stiffness, `--push_to_hub`) and [Command configuration](/cli/configuration) for the override syntax.
  </Tab>
</Tabs>

## Controller layout

Episodes are driven from the Quest controllers (the control panel can take the same decisions — see [Driving from the panel](#driving-from-the-panel)). The movement controls are the same as in [teleoperation](/operations/teleop); data collection adds the **A** (record) button. The session is locked to data collection mode, so there's no mode toggle to worry about.

<img src="https://mintcdn.com/almondbot/QoFJerlOUFU8RmP8/assets/quest.png?fit=max&auto=format&n=QoFJerlOUFU8RmP8&q=85&s=5e7d85aeb3afa2639107ac9ebc6bdd8f" alt="Quest controller diagram" width="370" height="210" data-path="assets/quest.png" />

| # | Button        | Action                                                                                                                                                                                                                                                                                                                                          |
| - | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Left grip     | Press both grips (1 + 2) together to **engage** tracking from rest; once engaged, each grip **toggles its own arm** between tracking and frozen (a frozen arm holds its pose and grasp)                                                                                                                                                         |
| 2 | Right grip    | See above                                                                                                                                                                                                                                                                                                                                       |
| 3 | Left trigger  | Actuate left gripper. While tracking is disengaged: point at a camera screen and hold to **move** it (both triggers on one screen **resize** it)                                                                                                                                                                                                |
| 4 | Right trigger | Actuate right gripper. While tracking is disengaged: point at a camera screen and hold to **move** it (both triggers on one screen **resize** it)                                                                                                                                                                                               |
| 5 | X             | **Reset** to rest pose — including out of the contact-fallback [gravity-comp hold](#recording-an-episode), where it sends the hand-guided arms home; during the start countdown, cancels it. During a recording, arms the **Discard episode?** confirmation — press **X** again to discard and re-record, or **A** to cancel and keep recording |
| 6 | A             | **Record**: start a take (3-second countdown). During a recording, arms the **Save episode?** confirmation — press **A** again to save, or **X** to cancel and keep recording                                                                                                                                                                   |
| 7 | Y             | **Exit** the VR session                                                                                                                                                                                                                                                                                                                         |
| — | Right **B**   | **Reset the camera screens** — re-anchor to the current gaze and clear moves + resizes; see [Camera views](/guides/vr-interface#camera-views)                                                                                                                                                                                                   |
| — | Thumbsticks   | **Drive the [powered cart](/guides/vr-interface#powered-cart)** (robots so equipped) to reposition between takes — operator mobility only, so cart motion is **never recorded** into the dataset                                                                                                                                                |

<Note>
  The camera feeds are relayed to the headset during collection, so you can line up takes from the robot's point of view.
</Note>

## Recording an episode

<Steps>
  <Step title="Drive to the start pose">
    The session is already in **Data Collection** — put on the headset, engage tracking, and drive the arms to where you want the take to begin.
  </Step>

  <Step title="Start a take">
    Press **A** to begin recording — a 3-second countdown runs, then frame capture begins. Press **A** again during the countdown, or **X**, to cancel.
  </Step>

  <Step title="Finish the take">
    Stopping is **confirmation-gated** — the first press arms an in-headset popup instead of stopping immediately, so a fumbled button never loses a take:

    * Press **A** to arm **Save episode?** — press **A** again to save, or **X** to cancel and keep recording.
    * Press **X** to arm **Discard episode?** — press **X** again to discard and re-record, or **A** to cancel and keep recording.

    Nothing is committed until you confirm. After you confirm a **save**, the headset enters the **Saving** state and every control except **Y** (exit) is blocked while the episode is written to disk. Confirming a **discard** drops the take and rewinds to re-record it.
  </Step>

  <Step title="The arms return home — guarded">
    After a take ends — saved or discarded — the arms head back to the rest pose on their own, but **guarded**: a torque watchdog compares every joint against the gravity model as the move plays.

    If the move meets **unexpected resistance** — a gripper still hooked on a handle, or you grabbing an arm — it stops where it is and the arms drop into a limp **gravity-compensation hold**, free to be hand-guided. Ease them clear of the scene (the episode keeps saving in the background), then press **X** to send them home again — the return replans **from wherever you left the arms**. Recording is blocked while limp; pressing **A** just gets you a reminder to reset first.

    The same guard covers **every** rest move — an **X** reset between takes and the startup move too, not just the post-episode return. Its sensitivity is the **Reset contact threshold** in the control panel's Robot settings (`--teleop_config.vr_teleop_config.reset_torque_threshold` on the CLI) — raise it if normal returns trip it falsely, or set `0` to disable it and always complete the return.

    You can also arm a watchdog for the **tracking** phase — while you're actively driving the arms during a take. It's **off by default** (recording pushes on the scene on purpose): set the **Teleop contact stop** in the control panel's Robot settings (`--teleop_config.vr_teleop_config.teleop_torque_threshold`; the panel suggests `16` Nm). A trip mid-take **disengages tracking, discards the in-flight episode**, and drops the arms limp — clear them by hand, then press **X** to return to rest and re-record. Shared with [`teleop`](/operations/teleop), so both flows behave identically.
  </Step>
</Steps>

The HUD shows an **Episode N** readout (top-right) with the 1-based number of the episode you're about to record, so you always know where you are in the dataset.

| Headset state     | Meaning                                         |
| ----------------- | ----------------------------------------------- |
| `Data Collection` | Driving the arms; ready to record               |
| `Recording`       | Capturing frames to the dataset                 |
| `Saving`          | Episode being written to disk; controls blocked |

Collection resumes from an existing dataset at `--root`; an aborted session that saved no episodes is cleaned up on shutdown.

## Driving from the panel

Everything above assumes the headset is on your face. When you launched the session from the [control panel](/guides/control-panel), it doubles as an operator display and can take the same decisions, so you can collect with the headset off — resting on the table, still holding the controllers.

The panel mirrors the **camera feeds** the headset receives (with a fullscreen toggle for a proper operator display) and an **Episode control** box carrying the phase, the episode number, the saved count, and the buttons that make sense right now: **Start recording** (a 3-second countdown mirroring the in-headset one, so you can pick the controllers back up; click again to cancel), **Save episode**, and **Discard episode** — the last two click-to-confirm, standing in for the controller's double-press. During a contact hold a **Return to rest** button appears, which does what **X** does: replan the return from wherever you left the arms.

The controller stays live the whole time — the panel is a parallel input, and whichever arrives first ends the take. Anything you arm on the controller (the confirmation popup, the record countdown) is mirrored into the panel too, so the dashboard follows a controller-driven session as well as a panel-driven one. See [Episode controls](/guides/control-panel#episode-controls).

<Note>
  **Crash-safe by design.** Every episode is durable the moment it saves — its parquet footers are flushed and recording rotates to fresh per-episode files — so at any point between saves the dataset on disk is complete and resumable, and killing the recorder costs at most the take being written. Finalizing on stop is near-instant now (no session-length flush or whole-session video re-verification), so there's no slow shutdown tempting a hard kill. A resume still runs a **crash-consistency check** for that one in-flight window (and for datasets recorded before durability landed): if the saved episodes aren't a contiguous `0…N-1` — the tell-tale of a recorder killed mid-save, where buffered episodes were lost while the counter had already advanced — the dataset is **repaired in place**, truncating back to the longest intact run of episodes and resuming after it (recording past the gap would silently pair every later episode with a different episode's video). Only if not a single complete episode survives does it refuse and point you to delete the dataset. [Keep the host on a current release](/installation#staying-up-to-date) to pick this up on an older station.
</Note>

<Note>
  **Saved takes are integrity-checked.** If the camera encoder drops frames during a take (a feed-queue overflow under heavy load), the video no longer lines up with its recorded rows — so that episode is **discarded instead of saved**, with an error explaining why (in the CLI output / control panel log console). Re-record it rather than adding a misaligned episode to the dataset.
</Note>

<Note>
  The full controller bindings and headset state machine are documented in the [VR Interface](/guides/vr-interface) guide.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Run Policy" icon="robot" href="/operations/run-policy">
    Run a trained policy autonomously on the same machine.
  </Card>

  <Card title="collect-data reference" icon="terminal" href="/cli/collect-data">
    Every flag and the dataset capture internals.
  </Card>
</CardGroup>
