2024-07-15 17:43:10 +02:00
"""
2024-08-15 18:11:33 +02:00
Utilities to control a robot .
Useful to record a dataset , replay a recorded episode , run the policy on your robot
and record an evaluation dataset , and to recalibrate your robot if needed .
2024-07-15 17:43:10 +02:00
Examples of usage :
2024-08-15 18:11:33 +02:00
- Recalibrate your robot :
` ` ` bash
python lerobot / scripts / control_robot . py calibrate
` ` `
2024-07-15 17:43:10 +02:00
- Unlimited teleoperation at highest frequency ( ~ 200 Hz is expected ) , to exit with CTRL + C :
` ` ` bash
python lerobot / scripts / control_robot . py teleoperate
2024-08-15 18:11:33 +02:00
# Remove the cameras from the robot definition. They are not used in 'teleoperate' anyway.
python lerobot / scripts / control_robot . py teleoperate - - robot - overrides ' ~cameras '
2024-07-15 17:43:10 +02:00
` ` `
- Unlimited teleoperation at a limited frequency of 30 Hz , to simulate data recording frequency :
` ` ` bash
python lerobot / scripts / control_robot . py teleoperate \
- - fps 30
` ` `
- Record one episode in order to test replay :
` ` ` bash
2024-08-15 18:11:33 +02:00
python lerobot / scripts / control_robot . py record \
2024-07-15 17:43:10 +02:00
- - fps 30 \
- - root tmp / data \
- - repo - id $ USER / koch_test \
- - num - episodes 1 \
- - run - compute - stats 0
` ` `
- Visualize dataset :
` ` ` bash
python lerobot / scripts / visualize_dataset . py \
- - root tmp / data \
- - repo - id $ USER / koch_test \
- - episode - index 0
` ` `
- Replay this test episode :
` ` ` bash
2024-08-15 18:11:33 +02:00
python lerobot / scripts / control_robot . py replay \
2024-07-15 17:43:10 +02:00
- - fps 30 \
- - root tmp / data \
- - repo - id $ USER / koch_test \
- - episode 0
` ` `
- Record a full dataset in order to train a policy , with 2 seconds of warmup ,
30 seconds of recording for each episode , and 10 seconds to reset the environment in between episodes :
` ` ` bash
2024-08-15 18:11:33 +02:00
python lerobot / scripts / control_robot . py record \
2024-07-15 17:43:10 +02:00
- - fps 30 \
- - root data \
- - repo - id $ USER / koch_pick_place_lego \
- - num - episodes 50 \
- - warmup - time - s 2 \
- - episode - time - s 30 \
- - reset - time - s 10
` ` `
* * NOTE * * : You can use your keyboard to control data recording flow .
- Tap right arrow key ' -> ' to early exit while recording an episode and go to resseting the environment .
- Tap right arrow key ' -> ' to early exit while resetting the environment and got to recording the next episode .
- Tap left arrow key ' <- ' to early exit and re - record the current episode .
- Tap escape key ' esc ' to stop the data recording .
This might require a sudo permission to allow your terminal to monitor keyboard events .
* * NOTE * * : You can resume / continue data recording by running the same data recording command twice .
To avoid resuming by deleting the dataset , use ` - - force - override 1 ` .
- Train on this dataset with the ACT policy :
` ` ` bash
DATA_DIR = data python lerobot / scripts / train . py \
policy = act_koch_real \
env = koch_real \
dataset_repo_id = $ USER / koch_pick_place_lego \
hydra . run . dir = outputs / train / act_koch_real
` ` `
- Run the pretrained policy on the robot :
` ` ` bash
2024-08-15 18:11:33 +02:00
python lerobot / scripts / control_robot . py record \
- - fps 30 \
- - root data \
- - repo - id $ USER / eval_act_koch_real \
- - num - episodes 10 \
- - warmup - time - s 2 \
- - episode - time - s 30 \
- - reset - time - s 10
2024-07-15 17:43:10 +02:00
- p outputs / train / act_koch_real / checkpoints / 080000 / pretrained_model
` ` `
"""
import argparse
import logging
import time
from pathlib import Path
2024-10-16 20:51:35 +02:00
from typing import List
2024-07-15 17:43:10 +02:00
# from safetensors.torch import load_file, save_file
2024-10-16 20:51:35 +02:00
from lerobot . common . datasets . lerobot_dataset import LeRobotDataset
from lerobot . common . datasets . populate_dataset import (
create_lerobot_dataset ,
delete_current_episode ,
init_dataset ,
save_current_episode ,
)
from lerobot . common . robot_devices . control_utils import (
control_loop ,
has_method ,
init_keyboard_listener ,
init_policy ,
log_control_info ,
record_episode ,
reset_environment ,
sanity_check_dataset_name ,
stop_recording ,
warmup_record ,
)
2024-07-15 17:43:10 +02:00
from lerobot . common . robot_devices . robots . factory import make_robot
2024-10-16 20:51:35 +02:00
from lerobot . common . robot_devices . robots . utils import Robot
2024-10-04 18:56:42 +02:00
from lerobot . common . robot_devices . utils import busy_wait , safe_disconnect
2024-10-16 20:51:35 +02:00
from lerobot . common . utils . utils import init_hydra_config , init_logging , log_say , none_or_int
2024-10-10 17:12:45 +02:00
2024-10-04 18:56:42 +02:00
########################################################################################
# Control modes
########################################################################################
@safe_disconnect
2024-10-03 17:05:23 +02:00
def calibrate ( robot : Robot , arms : list [ str ] | None ) :
2024-10-04 18:56:42 +02:00
# TODO(aliberts): move this code in robots' classes
if robot . robot_type . startswith ( " stretch " ) :
if not robot . is_connected :
robot . connect ( )
if not robot . is_homed ( ) :
robot . home ( )
return
2024-10-25 11:23:55 +02:00
if arms is None :
arms = robot . available_arms
2024-10-16 20:51:35 +02:00
unknown_arms = [ arm_id for arm_id in arms if arm_id not in robot . available_arms ]
available_arms_str = " " . join ( robot . available_arms )
2024-09-04 19:28:05 +02:00
unknown_arms_str = " " . join ( unknown_arms )
if arms is None or len ( arms ) == 0 :
raise ValueError (
" No arm provided. Use `--arms` as argument with one or more available arms. \n "
f " For instance, to recalibrate all arms add: `--arms { available_arms_str } ` "
)
if len ( unknown_arms ) > 0 :
raise ValueError (
f " Unknown arms provided ( ' { unknown_arms_str } ' ). Available arms are ` { available_arms_str } `. "
)
for arm_id in arms :
arm_calib_path = robot . calibration_dir / f " { arm_id } .json "
if arm_calib_path . exists ( ) :
print ( f " Removing ' { arm_calib_path } ' " )
arm_calib_path . unlink ( )
else :
print ( f " Calibration file not found ' { arm_calib_path } ' " )
2024-08-15 18:11:33 +02:00
if robot . is_connected :
robot . disconnect ( )
# Calling `connect` automatically runs calibration
# when the calibration file is missing
robot . connect ( )
2024-09-04 19:28:05 +02:00
robot . disconnect ( )
print ( " Calibration is done! You can now teleoperate and record datasets! " )
2024-08-15 18:11:33 +02:00
2024-10-04 18:56:42 +02:00
@safe_disconnect
2024-10-16 20:51:35 +02:00
def teleoperate (
robot : Robot , fps : int | None = None , teleop_time_s : float | None = None , display_cameras : bool = False
) :
control_loop (
robot ,
control_time_s = teleop_time_s ,
fps = fps ,
teleoperate = True ,
display_cameras = display_cameras ,
)
2024-07-15 17:43:10 +02:00
2024-10-04 18:56:42 +02:00
@safe_disconnect
2024-08-15 18:11:33 +02:00
def record (
2024-07-15 17:43:10 +02:00
robot : Robot ,
2024-10-16 20:51:35 +02:00
root : str ,
repo_id : str ,
pretrained_policy_name_or_path : str | None = None ,
policy_overrides : List [ str ] | None = None ,
2024-07-15 17:43:10 +02:00
fps : int | None = None ,
warmup_time_s = 2 ,
episode_time_s = 10 ,
reset_time_s = 5 ,
num_episodes = 50 ,
video = True ,
run_compute_stats = True ,
push_to_hub = True ,
2024-08-16 10:08:44 +02:00
tags = None ,
2024-10-10 17:12:45 +02:00
num_image_writer_processes = 0 ,
num_image_writer_threads_per_camera = 4 ,
2024-07-15 17:43:10 +02:00
force_override = False ,
2024-10-03 17:05:23 +02:00
display_cameras = True ,
2024-10-10 17:12:45 +02:00
play_sounds = True ,
2024-07-15 17:43:10 +02:00
) :
# TODO(rcadene): Add option to record logs
2024-10-16 20:51:35 +02:00
listener = None
events = None
policy = None
device = None
use_amp = None
# Load pretrained policy
if pretrained_policy_name_or_path is not None :
policy , policy_fps , device , use_amp = init_policy ( pretrained_policy_name_or_path , policy_overrides )
2025-04-15 15:41:24 +02:00
device = " mps "
policy . to ( device )
2024-10-16 20:51:35 +02:00
if fps is None :
fps = policy_fps
logging . warning ( f " No fps provided, so using the fps from policy config ( { policy_fps } ). " )
elif fps != policy_fps :
logging . warning (
f " There is a mismatch between the provided fps ( { fps } ) and the one from policy config ( { policy_fps } ). "
)
# Create empty dataset or load existing saved episodes
sanity_check_dataset_name ( repo_id , policy )
dataset = init_dataset (
repo_id ,
root ,
force_override ,
fps ,
video ,
write_images = robot . has_camera ,
num_image_writer_processes = num_image_writer_processes ,
num_image_writer_threads = num_image_writer_threads_per_camera * robot . num_cameras ,
)
2024-07-15 17:43:10 +02:00
if not robot . is_connected :
robot . connect ( )
2024-10-16 20:51:35 +02:00
listener , events = init_keyboard_listener ( )
2024-08-15 18:11:33 +02:00
2024-10-16 20:51:35 +02:00
# Execute a few seconds without recording to:
# 1. teleoperate the robot to move it in starting position if no policy provided,
# 2. give times to the robot devices to connect and start synchronizing,
# 3. place the cameras windows on screen
enable_teleoperation = policy is None
log_say ( " Warmup record " , play_sounds )
2025-04-13 14:10:27 +02:00
# warmup_record(robot, events, enable_teleoperation, warmup_time_s, display_cameras, fps)
2024-08-15 18:11:33 +02:00
2024-10-04 18:56:42 +02:00
if has_method ( robot , " teleop_safety_stop " ) :
robot . teleop_safety_stop ( )
2024-10-16 20:51:35 +02:00
while True :
if dataset [ " num_episodes " ] > = num_episodes :
break
episode_index = dataset [ " num_episodes " ]
log_say ( f " Recording episode { episode_index } " , play_sounds )
record_episode (
dataset = dataset ,
robot = robot ,
events = events ,
episode_time_s = episode_time_s ,
display_cameras = display_cameras ,
policy = policy ,
device = device ,
use_amp = use_amp ,
fps = fps ,
2024-10-10 17:12:45 +02:00
)
2024-10-16 20:51:35 +02:00
# Execute a few seconds without recording to give time to manually reset the environment
# Current code logic doesn't allow to teleoperate during this time.
# TODO(rcadene): add an option to enable teleoperation during reset
# Skip reset for the last episode to be recorded
if not events [ " stop_recording " ] and (
( episode_index < num_episodes - 1 ) or events [ " rerecord_episode " ]
) :
log_say ( " Reset the environment " , play_sounds )
reset_environment ( robot , events , reset_time_s )
if events [ " rerecord_episode " ] :
log_say ( " Re-record episode " , play_sounds )
events [ " rerecord_episode " ] = False
events [ " exit_early " ] = False
delete_current_episode ( dataset )
continue
# Increment by one dataset["current_episode_index"]
save_current_episode ( dataset )
if events [ " stop_recording " ] :
break
2024-07-15 17:43:10 +02:00
2024-10-16 20:51:35 +02:00
log_say ( " Stop recording " , play_sounds , blocking = True )
stop_recording ( robot , listener , display_cameras )
2024-10-10 17:12:45 +02:00
2024-10-16 20:51:35 +02:00
lerobot_dataset = create_lerobot_dataset ( dataset , run_compute_stats , push_to_hub , tags , play_sounds )
log_say ( " Exiting " , play_sounds )
2024-07-15 17:43:10 +02:00
return lerobot_dataset
2024-10-16 20:51:35 +02:00
@safe_disconnect
2024-10-10 17:12:45 +02:00
def replay (
robot : Robot , episode : int , fps : int | None = None , root = " data " , repo_id = " lerobot/debug " , play_sounds = True
) :
2024-10-16 20:51:35 +02:00
# TODO(rcadene, aliberts): refactor with control_loop, once `dataset` is an instance of LeRobotDataset
2024-07-15 17:43:10 +02:00
# TODO(rcadene): Add option to record logs
local_dir = Path ( root ) / repo_id
if not local_dir . exists ( ) :
raise ValueError ( local_dir )
dataset = LeRobotDataset ( repo_id , root = root )
items = dataset . hf_dataset . select_columns ( " action " )
from_idx = dataset . episode_data_index [ " from " ] [ episode ] . item ( )
to_idx = dataset . episode_data_index [ " to " ] [ episode ] . item ( )
if not robot . is_connected :
robot . connect ( )
2024-10-16 20:51:35 +02:00
log_say ( " Replaying episode " , play_sounds , blocking = True )
2024-07-15 17:43:10 +02:00
for idx in range ( from_idx , to_idx ) :
2024-08-15 18:11:33 +02:00
start_episode_t = time . perf_counter ( )
2024-07-15 17:43:10 +02:00
action = items [ idx ] [ " action " ]
robot . send_action ( action )
2024-08-15 18:11:33 +02:00
dt_s = time . perf_counter ( ) - start_episode_t
2024-07-15 17:43:10 +02:00
busy_wait ( 1 / fps - dt_s )
2024-08-15 18:11:33 +02:00
dt_s = time . perf_counter ( ) - start_episode_t
2024-07-15 17:43:10 +02:00
log_control_info ( robot , dt_s , fps = fps )
if __name__ == " __main__ " :
parser = argparse . ArgumentParser ( )
subparsers = parser . add_subparsers ( dest = " mode " , required = True )
# Set common options for all the subparsers
base_parser = argparse . ArgumentParser ( add_help = False )
base_parser . add_argument (
2024-08-15 18:11:33 +02:00
" --robot-path " ,
type = str ,
default = " lerobot/configs/robot/koch.yaml " ,
help = " Path to robot yaml file used to instantiate the robot using `make_robot` factory function. " ,
)
base_parser . add_argument (
" --robot-overrides " ,
2024-07-15 17:43:10 +02:00
type = str ,
2024-08-15 18:11:33 +02:00
nargs = " * " ,
help = " Any key=value arguments to override config values (use dots for.nested=overrides) " ,
2024-07-15 17:43:10 +02:00
)
2024-08-15 18:11:33 +02:00
parser_calib = subparsers . add_parser ( " calibrate " , parents = [ base_parser ] )
2024-09-04 19:28:05 +02:00
parser_calib . add_argument (
" --arms " ,
2024-09-06 08:44:31 -04:00
type = str ,
2024-09-04 19:28:05 +02:00
nargs = " * " ,
help = " List of arms to calibrate (e.g. `--arms left_follower right_follower left_leader`) " ,
)
2024-08-15 18:11:33 +02:00
2024-07-15 17:43:10 +02:00
parser_teleop = subparsers . add_parser ( " teleoperate " , parents = [ base_parser ] )
parser_teleop . add_argument (
" --fps " , type = none_or_int , default = None , help = " Frames per second (set to None to disable) "
)
2024-10-16 20:51:35 +02:00
parser_teleop . add_argument (
" --display-cameras " ,
type = int ,
default = 1 ,
help = " Display all cameras on screen (set to 1 to display or 0). " ,
)
2024-07-15 17:43:10 +02:00
2024-08-15 18:11:33 +02:00
parser_record = subparsers . add_parser ( " record " , parents = [ base_parser ] )
2024-07-15 17:43:10 +02:00
parser_record . add_argument (
" --fps " , type = none_or_int , default = None , help = " Frames per second (set to None to disable) "
)
parser_record . add_argument (
" --root " ,
type = Path ,
default = " data " ,
help = " Root directory where the dataset will be stored locally at ' {root} / {repo_id} ' (e.g. ' data/hf_username/dataset_name ' ). " ,
)
parser_record . add_argument (
" --repo-id " ,
type = str ,
default = " lerobot/test " ,
help = " Dataset identifier. By convention it should match ' {hf_username} / {dataset_name} ' (e.g. `lerobot/test`). " ,
)
parser_record . add_argument (
" --warmup-time-s " ,
type = int ,
2024-08-15 18:11:33 +02:00
default = 10 ,
2024-07-15 17:43:10 +02:00
help = " Number of seconds before starting data collection. It allows the robot devices to warmup and synchronize. " ,
)
parser_record . add_argument (
" --episode-time-s " ,
type = int ,
2024-08-15 18:11:33 +02:00
default = 60 ,
2024-07-15 17:43:10 +02:00
help = " Number of seconds for data recording for each episode. " ,
)
parser_record . add_argument (
" --reset-time-s " ,
type = int ,
2024-08-15 18:11:33 +02:00
default = 60 ,
2024-07-15 17:43:10 +02:00
help = " Number of seconds for resetting the environment after each episode. " ,
)
parser_record . add_argument ( " --num-episodes " , type = int , default = 50 , help = " Number of episodes to record. " )
parser_record . add_argument (
" --run-compute-stats " ,
type = int ,
default = 1 ,
help = " By default, run the computation of the data statistics at the end of data collection. Compute intensive and not required to just replay an episode. " ,
)
parser_record . add_argument (
" --push-to-hub " ,
type = int ,
default = 1 ,
help = " Upload dataset to Hugging Face hub. " ,
)
2024-08-16 10:08:44 +02:00
parser_record . add_argument (
" --tags " ,
type = str ,
nargs = " * " ,
help = " Add tags to your dataset on the hub. " ,
)
2024-07-15 17:43:10 +02:00
parser_record . add_argument (
2024-10-10 17:12:45 +02:00
" --num-image-writer-processes " ,
type = int ,
default = 0 ,
help = (
" Number of subprocesses handling the saving of frames as PNGs. Set to 0 to use threads only; "
" set to ≥1 to use subprocesses, each using threads to write images. The best number of processes "
" and threads depends on your system. We recommend 4 threads per camera with 0 processes. "
" If fps is unstable, adjust the thread count. If still unstable, try using 1 or more subprocesses. "
) ,
)
parser_record . add_argument (
" --num-image-writer-threads-per-camera " ,
2024-07-15 17:43:10 +02:00
type = int ,
2024-09-12 14:20:24 +02:00
default = 4 ,
help = (
" Number of threads writing the frames as png images on disk, per camera. "
2024-10-10 17:12:45 +02:00
" Too many threads might cause unstable teleoperation fps due to main thread being blocked. "
2024-09-12 14:20:24 +02:00
" Not enough threads might cause low camera fps. "
) ,
2024-07-15 17:43:10 +02:00
)
parser_record . add_argument (
" --force-override " ,
type = int ,
default = 0 ,
help = " By default, data recording is resumed. When set to 1, delete the local directory and start data recording from scratch. " ,
)
2024-08-15 18:11:33 +02:00
parser_record . add_argument (
" -p " ,
" --pretrained-policy-name-or-path " ,
type = str ,
help = (
" Either the repo ID of a model hosted on the Hub or a path to a directory containing weights "
" saved using `Policy.save_pretrained`. "
) ,
)
parser_record . add_argument (
" --policy-overrides " ,
type = str ,
nargs = " * " ,
help = " Any key=value arguments to override config values (use dots for.nested=overrides) " ,
)
2024-07-15 17:43:10 +02:00
2024-08-15 18:11:33 +02:00
parser_replay = subparsers . add_parser ( " replay " , parents = [ base_parser ] )
2024-07-15 17:43:10 +02:00
parser_replay . add_argument (
" --fps " , type = none_or_int , default = None , help = " Frames per second (set to None to disable) "
)
parser_replay . add_argument (
" --root " ,
type = Path ,
default = " data " ,
help = " Root directory where the dataset will be stored locally at ' {root} / {repo_id} ' (e.g. ' data/hf_username/dataset_name ' ). " ,
)
parser_replay . add_argument (
" --repo-id " ,
type = str ,
default = " lerobot/test " ,
help = " Dataset identifier. By convention it should match ' {hf_username} / {dataset_name} ' (e.g. `lerobot/test`). " ,
)
parser_replay . add_argument ( " --episode " , type = int , default = 0 , help = " Index of the episode to replay. " )
args = parser . parse_args ( )
init_logging ( )
control_mode = args . mode
2024-08-15 18:11:33 +02:00
robot_path = args . robot_path
robot_overrides = args . robot_overrides
2024-07-15 17:43:10 +02:00
kwargs = vars ( args )
del kwargs [ " mode " ]
2024-08-15 18:11:33 +02:00
del kwargs [ " robot_path " ]
del kwargs [ " robot_overrides " ]
2025-04-15 15:41:24 +02:00
2024-08-15 18:11:33 +02:00
robot_cfg = init_hydra_config ( robot_path , robot_overrides )
robot = make_robot ( robot_cfg )
2024-07-15 17:43:10 +02:00
2024-08-15 18:11:33 +02:00
if control_mode == " calibrate " :
calibrate ( robot , * * kwargs )
elif control_mode == " teleoperate " :
2024-07-15 17:43:10 +02:00
teleoperate ( robot , * * kwargs )
2024-08-15 18:11:33 +02:00
elif control_mode == " record " :
2024-10-16 20:51:35 +02:00
record ( robot , * * kwargs )
2024-08-15 18:11:33 +02:00
elif control_mode == " replay " :
replay ( robot , * * kwargs )
if robot . is_connected :
# Disconnect manually to avoid a "Core dump" during process
# termination due to camera threads not properly exiting.
robot . disconnect ( )