Skip to content

Teleoperate and Record Datasets

Armnet lets you drive a managed cell's robot in real time from LeRobot leader arm(s) plugged into your own machine, and — when you're ready — record that teleoperation into a LeRobot dataset on the cell. Two paired commands cover the workflow:

Command What it does Records data?
armnet-lerobot-teleop Drive the remote follower and watch it live in Rerun. No
armnet-lerobot-record The same teleop loop, but the cell records each episode into a LeRobotDataset and pushes it to the Hub. Yes

Both stream the follower's cameras and joint state back into a live Rerun viewer so you can see what the remote arm is doing as you move the leader. Use teleop alone to validate a cell, line up a scene, or rehearse a manipulation; switch to record once the motion is good.

How it works

leader arm (your desk)   sampled at ~60Hz by the CLI
                           -> /jobs/{job_id}/teleop websocket (freshest-wins)
orchestrator             forwards each action to the job's cell
cell follower            applies the freshest teleop action each control tick
                           (armnet-lerobot-record also writes obs/action
                            pairs into a LeRobotDataset on the cell)
your machine             follower cameras + joints stream back over Rerun

The leader is sampled faster than the follower's control loop (60Hz vs ~20Hz) so the cell always has a near-current command — a dropped or late frame never makes the follower wait a full tick. Recorded data never touches your machine: the dataset is built on the cell and pushed to the Hub from there.

Prerequisites

  • A LeRobot leader arm (e.g. an SO-101 leader) plugged into your machine. For a bimanual SO-101 cell, plug in both local leader arms.
  • The teleop extra, which brings in LeRobot for the local leader:
pip install 'armnet-client[teleop]'   # or: uv pip install 'armnet-client[teleop]'
  • A configured API key (see Installation).
  • A leader arm that is already calibrated. The CLI does not recalibrate on connect; calibrate it once with LeRobot's standard tooling and pass its teleoperator id so the calibration is found.

The leader arm connection

Both commands take the same --teleop.* flags, which mirror what LeRobot needs to construct a teleoperator:

Flag Default Purpose
--teleop.port (none) Serial port of the local single-arm leader, e.g. /dev/ttyACM0.
--teleop.left-port (none) Serial port of the local left leader for bimanual teleop.
--teleop.right-port (none) Serial port of the local right leader for bimanual teleop.
--teleop.id (none) LeRobot teleoperator id, used to locate the leader's calibration.
--teleop.fps 60 Leader sampling rate in Hz (intentionally higher than the follower loop).

Provide either --teleop.port for a single-arm cell, or both --teleop.left-port and --teleop.right-port for a bimanual cell. Do not pass all three.

Find the leader's serial device with:

ls -l /dev/serial/by-id/

Remote teleop currently supports the lerobot/so-101 embodiment.

Teleoperate (no recording)

armnet-lerobot-teleop \
  --teleop.port=/dev/ttyACM0 \
  --teleop.id=my_leader \
  --armnet.duration_s=300

For a bimanual SO-101 cell:

armnet-lerobot-teleop \
  --teleop.left-port=/dev/ttyACM0 \
  --teleop.right-port=/dev/ttyACM1 \
  --teleop.id=my_bimanual_leader \
  --armnet.task=insert_big_rings \
  --armnet.duration_s=300

The local BiSOLeader emits left_... and right_... joint keys, matching the remote cell's BiSOFollower.

A Rerun viewer opens, the job is submitted to an available SO-101 cell, and once it starts you drive the remote follower by moving your leader. Useful flags:

Flag Default Purpose
--armnet.embodiment lerobot/so-101 Robot embodiment.
--armnet.task (none) Cell task slug; omit to let any cell of the embodiment pick the job up.
--armnet.fps 20 Follower control rate on the cell.
--armnet.duration_s 300 How long the teleop session runs.
--secret ENV=SECRET_NAME Inject a stored secret as an env var in the cell container (repeatable).
--timeout-seconds (derived) Wall-clock cap on the job; derived from the duration when omitted.

Record a dataset

armnet-lerobot-record runs the same teleop loop, but the cell records each episode into a LeRobotDataset and pushes it to the Hugging Face Hub when the session ends:

armnet-lerobot-record \
  --teleop.port=/dev/ttyACM0 \
  --teleop.id=my_leader \
  --dataset.repo_id=my_user/so101_pick_place \
  --dataset.single_task="Grab the black cube" \
  --dataset.num_episodes=10 \
  --secret HF_TOKEN=huggingface-token

For bimanual recording, use the same left/right teleop ports:

armnet-lerobot-record \
  --teleop.left-port=/dev/ttyACM0 \
  --teleop.right-port=/dev/ttyACM1 \
  --teleop.id=my_bimanual_leader \
  --armnet.task=<bimanual-task-slug> \
  --dataset.repo_id=my_user/so101_bimanual_insert_rings \
  --dataset.single_task="Insert the colourful rings into the central wooden peg" \
  --dataset.num_episodes=10 \
  --secret HF_TOKEN=huggingface-token

Bimanual jobs should target the configured bimanual task with --armnet.task so they do not route to a single-arm SO-101 cell.

Store your Hugging Face token as a secret first (armnet secret create huggingface-token hf_...) and request it with --secret HF_TOKEN=huggingface-token so the cell can push the dataset.

Recording controls

LeRobot's standard dataset-recording keyboard shortcuts work during the session and are forwarded to the cell over the teleop channel:

  • Right Arrow — save the current episode. The remote arm returns to its rest position and recording resumes once an operator at the cell confirms the workspace reset.
  • Left Arrow — discard the current episode and re-record it (same rest + reset-confirmation flow).
  • Esc — stop the session. The dataset is finalized on the cell and pushed to the Hub (unless --no-push-to-hub is set).

Keyboard controls need a display

The shortcuts use a global keyboard listener (pynput), which needs a desktop session. On a headless Linux host without $DISPLAY they're unavailable — the same limitation LeRobot's own recorder has.

Dataset options

Flag Default Purpose
--dataset.repo_id (generated) Hub repo id, e.g. user/so101_pick_place.
--dataset.single_task (none) Language instruction stored on every frame.
--dataset.num_episodes 5 Number of episodes to record.
--dataset.episode_time_s 120 Per-episode cap; Right Arrow ends one early.
--dataset.fps 30 Recording/control rate on the cell.
--dataset.vcodec libsvtav1 Video codec on the cell. Software libsvtav1 honours the g=2 keyframe interval (fast random-access training reads); auto uses a hardware encoder when available but emits a large GOP unsuitable for training.
--dataset.encoder-threads 2 Threads per camera video encoder on the cell.
--dataset.no-streaming-encoding off Encode at episode save instead of in real time (slower saves).
--no-push-to-hub off Keep the dataset on the cell; skip the Hub upload.
--private-to-hub off Upload the dataset as private (default: public).
--hf-user (none) Hub namespace used when generating a repo id.

Camera frames are encoded to video in real time on the cell (LeRobot streaming encoding), so saving an episode is near-instant.

Headless machines

armnet-lerobot-teleop and armnet-lerobot-record open a native Rerun window by default. On an SSH / headless host, Rerun automatically falls back to its web viewer and prints how to reach it; see Visualize a Job with Rerun for the port-forwarding details. Note that the recording keyboard shortcuts still require a local display.

See Also