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-hubis 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¶
- Visualize a Job with Rerun — the live viewer both commands stream into.
- Evaluate an OpenPI Policy / Train and Deploy a LeRobot Policy — evaluate the policies you train on the datasets you record here.