Add Foxglove display mode to lerobot-record

Wire the --display_mode flag (rerun|foxglove) into lerobot-record, matching
lerobot-teleoperate: route init/log through the backend-agnostic dispatchers
and stop the visualization backend on exit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Roman Shtylman
2026-05-29 09:49:05 -07:00
committed by CarolinePascal
parent 3062bf0548
commit 5ed0981bc5
+28 -7
View File
@@ -38,6 +38,9 @@ lerobot-record \\
--display_data=true
```
To stream the data to Foxglove instead of Rerun, add ``--display_mode=foxglove`` (then connect the
Foxglove app to ``ws://127.0.0.1:8765``; override the port with ``--display_port=<port>``).
Example recording with bimanual so100:
```shell
lerobot-record \\
@@ -157,7 +160,11 @@ from lerobot.utils.utils import (
init_logging,
log_say,
)
from lerobot.utils.visualization_utils import init_rerun, log_rerun_data
from lerobot.utils.visualization_utils import (
init_visualization,
log_visualization_data,
shutdown_visualization,
)
@dataclass
@@ -168,11 +175,14 @@ class RecordConfig:
teleop: TeleoperatorConfig | None = None
# Display all cameras on screen
display_data: bool = False
# Display data on a remote Rerun server
# Visualization backend used when display_data is True: "rerun" or "foxglove".
# "foxglove" starts a WebSocket server (default ws://127.0.0.1:8765) to stream data to the Foxglove app.
display_mode: str = "rerun"
# For "rerun": IP of a remote Rerun server to connect to. Unused by "foxglove".
display_ip: str | None = None
# Port of the remote Rerun server
# For "rerun": port of the remote Rerun server. For "foxglove": port to bind the WebSocket server to.
display_port: int | None = None
# Whether to display compressed images in Rerun
# Whether to display compressed (JPEG) images instead of raw frames
display_compressed_images: bool = False
# Use vocal synthesis to read events.
play_sounds: bool = True
@@ -233,6 +243,7 @@ def record_loop(
control_time_s: int | None = None,
single_task: str | None = None,
display_data: bool = False,
display_mode: str = "rerun",
display_compressed_images: bool = False,
):
if dataset is not None and dataset.fps != fps:
@@ -327,8 +338,11 @@ def record_loop(
dataset.add_frame(frame)
if display_data:
log_rerun_data(
observation=obs_processed, action=action_values, compress_images=display_compressed_images
log_visualization_data(
display_mode,
observation=obs_processed,
action=action_values,
compress_images=display_compressed_images,
)
dt_s = time.perf_counter() - start_loop_t
@@ -354,7 +368,9 @@ def record(
init_logging()
logging.info(pformat(asdict(cfg)))
if cfg.display_data:
init_rerun(session_name="recording", ip=cfg.display_ip, port=cfg.display_port)
init_visualization(
cfg.display_mode, session_name="recording", ip=cfg.display_ip, port=cfg.display_port
)
display_compressed_images = (
True
if (cfg.display_data and cfg.display_ip is not None and cfg.display_port is not None)
@@ -464,6 +480,7 @@ def record(
control_time_s=cfg.dataset.episode_time_s,
single_task=cfg.dataset.single_task,
display_data=cfg.display_data,
display_mode=cfg.display_mode,
display_compressed_images=display_compressed_images,
)
@@ -485,6 +502,7 @@ def record(
control_time_s=cfg.dataset.reset_time_s,
single_task=cfg.dataset.single_task,
display_data=cfg.display_data,
display_mode=cfg.display_mode,
)
if events["rerecord_episode"]:
@@ -510,6 +528,9 @@ def record(
if listener is not None:
listener.stop()
if cfg.display_data:
shutdown_visualization(cfg.display_mode)
if cfg.dataset.push_to_hub:
if dataset and dataset.num_episodes > 0:
dataset.push_to_hub(tags=cfg.dataset.tags, private=cfg.dataset.private)