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 --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: Example recording with bimanual so100:
```shell ```shell
lerobot-record \\ lerobot-record \\
@@ -157,7 +160,11 @@ from lerobot.utils.utils import (
init_logging, init_logging,
log_say, 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 @dataclass
@@ -168,11 +175,14 @@ class RecordConfig:
teleop: TeleoperatorConfig | None = None teleop: TeleoperatorConfig | None = None
# Display all cameras on screen # Display all cameras on screen
display_data: bool = False 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 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 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 display_compressed_images: bool = False
# Use vocal synthesis to read events. # Use vocal synthesis to read events.
play_sounds: bool = True play_sounds: bool = True
@@ -233,6 +243,7 @@ def record_loop(
control_time_s: int | None = None, control_time_s: int | None = None,
single_task: str | None = None, single_task: str | None = None,
display_data: bool = False, display_data: bool = False,
display_mode: str = "rerun",
display_compressed_images: bool = False, display_compressed_images: bool = False,
): ):
if dataset is not None and dataset.fps != fps: if dataset is not None and dataset.fps != fps:
@@ -327,8 +338,11 @@ def record_loop(
dataset.add_frame(frame) dataset.add_frame(frame)
if display_data: if display_data:
log_rerun_data( log_visualization_data(
observation=obs_processed, action=action_values, compress_images=display_compressed_images display_mode,
observation=obs_processed,
action=action_values,
compress_images=display_compressed_images,
) )
dt_s = time.perf_counter() - start_loop_t dt_s = time.perf_counter() - start_loop_t
@@ -354,7 +368,9 @@ def record(
init_logging() init_logging()
logging.info(pformat(asdict(cfg))) logging.info(pformat(asdict(cfg)))
if cfg.display_data: 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 = ( display_compressed_images = (
True True
if (cfg.display_data and cfg.display_ip is not None and cfg.display_port is not None) 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, control_time_s=cfg.dataset.episode_time_s,
single_task=cfg.dataset.single_task, single_task=cfg.dataset.single_task,
display_data=cfg.display_data, display_data=cfg.display_data,
display_mode=cfg.display_mode,
display_compressed_images=display_compressed_images, display_compressed_images=display_compressed_images,
) )
@@ -485,6 +502,7 @@ def record(
control_time_s=cfg.dataset.reset_time_s, control_time_s=cfg.dataset.reset_time_s,
single_task=cfg.dataset.single_task, single_task=cfg.dataset.single_task,
display_data=cfg.display_data, display_data=cfg.display_data,
display_mode=cfg.display_mode,
) )
if events["rerecord_episode"]: if events["rerecord_episode"]:
@@ -510,6 +528,9 @@ def record(
if listener is not None: if listener is not None:
listener.stop() listener.stop()
if cfg.display_data:
shutdown_visualization(cfg.display_mode)
if cfg.dataset.push_to_hub: if cfg.dataset.push_to_hub:
if dataset and dataset.num_episodes > 0: if dataset and dataset.num_episodes > 0:
dataset.push_to_hub(tags=cfg.dataset.tags, private=cfg.dataset.private) dataset.push_to_hub(tags=cfg.dataset.tags, private=cfg.dataset.private)