| """ |
| All-in-one tmux launcher for SONIC data collection. |
| |
| Starts the full data collection stack in a single tmux session: |
| |
| Window 0 — data_collection (4 panes): |
| ┌───────────────────────┬───────────────────────┐ |
| │ Pane 0: C++ Deploy │ Pane 2: Data Exporter │ |
| │ (gear_sonic_deploy) │ (.venv_data_collection)│ |
| ├───────────────────────┼───────────────────────┤ |
| │ Pane 1: Teleop │ Pane 3: Camera Viewer │ |
| │ (.venv_teleop) │ (.venv_data_collection)│ |
| └───────────────────────┴───────────────────────┘ |
| |
| Window 1 — sim (only when --sim is passed): |
| ┌─────────────────────────────────────────────────┐ |
| │ MuJoCo Simulator (run_sim_loop.py) │ |
| │ (.venv_sim) │ |
| └─────────────────────────────────────────────────┘ |
| |
| Prerequisites: |
| - tmux installed (sudo apt install tmux) |
| - Virtual environments set up: |
| bash install_scripts/install_pico.sh -> .venv_teleop |
| bash install_scripts/install_data_collection.sh -> .venv_data_collection |
| - gear_sonic_deploy built (see docs) |
| - For sim: .venv_sim must exist (see install instructions) |
| |
| Usage (from repo root — no venv activation needed): |
| python gear_sonic/scripts/launch_data_collection.py # real robot (default) |
| python gear_sonic/scripts/launch_data_collection.py --sim # MuJoCo sim |
| python gear_sonic/scripts/launch_data_collection.py --no-camera-viewer # skip viewer |
| python gear_sonic/scripts/launch_data_collection.py --pico-input-source isaac-teleop # in-process CloudXR / DeviceIO |
| """ |
|
|
| from dataclasses import dataclass |
| from pathlib import Path |
| import os |
| import shutil |
| import signal |
| import socket |
| import subprocess |
| import sys |
| import time |
|
|
|
|
| def _bootstrap_venv(): |
| """Re-exec with the .venv_data_collection Python if tyro is not available.""" |
| try: |
| import tyro |
| return |
| except ImportError: |
| pass |
|
|
| repo_root = Path(__file__).resolve().parent.parent.parent |
| venv_python = repo_root / ".venv_data_collection" / "bin" / "python" |
| if not venv_python.exists(): |
| print( |
| "ERROR: tyro is not installed and .venv_data_collection not found.\n" |
| " Run: bash install_scripts/install_data_collection.sh" |
| ) |
| sys.exit(1) |
|
|
| print(f"Re-launching with {venv_python} ...") |
| os.execv(str(venv_python), [str(venv_python)] + sys.argv) |
|
|
|
|
| _bootstrap_venv() |
|
|
| import tyro |
|
|
|
|
| def _get_local_ip() -> str: |
| """Best-effort detection of the PC's LAN IP address.""" |
| try: |
| s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| s.connect(("8.8.8.8", 80)) |
| ip = s.getsockname()[0] |
| s.close() |
| return ip |
| except Exception: |
| return "unknown" |
|
|
|
|
| @dataclass |
| class DataCollectionLaunchConfig: |
| """CLI config for the all-in-one data collection tmux launcher.""" |
|
|
| |
| sim: bool = False |
| """Run against MuJoCo sim (deploy.sh sim) instead of real robot.""" |
|
|
| |
| deploy_input_type: str = "zmq_manager" |
| """Input type for the C++ deploy (zmq_manager, keyboard, etc.).""" |
|
|
| deploy_zmq_host: str = "localhost" |
| """ZMQ host for the C++ deploy to listen on.""" |
|
|
| deploy_checkpoint: str = "" |
| """Checkpoint path for deploy.sh (e.g., 'policy/checkpoints/my_model/model_step_100000'). |
| Leave empty to use the deploy.sh default.""" |
|
|
| deploy_obs_config: str = "" |
| """Observation config file for deploy.sh. Leave empty for default.""" |
|
|
| deploy_planner: str = "" |
| """Planner model path for deploy.sh. Leave empty for default.""" |
|
|
| deploy_motion_data: str = "" |
| """Motion data path for deploy.sh. Leave empty for default.""" |
|
|
| deploy_output_type: str = "" |
| """Output type for deploy.sh. Leave empty for default.""" |
|
|
| |
| pico_manager: bool = True |
| """Run pico_manager_thread_server with --manager flag.""" |
|
|
| pico_input_source: str = "xrt" |
| """Teleop input source for pico_manager_thread_server.py (xrt or isaac-teleop).""" |
|
|
| pico_vis_vr3pt: bool = False |
| """Enable VR 3-point visualization on the teleop streamer.""" |
|
|
| pico_vis_smpl: bool = False |
| """Enable SMPL visualization on the teleop streamer.""" |
|
|
| pico_waist_tracking: bool = False |
| """Enable waist tracking on the teleop streamer.""" |
|
|
| |
| task_prompt: str = "demo" |
| """Language task prompt for the data exporter.""" |
|
|
| dataset_name: str = "" |
| """Dataset name for the data exporter. Leave empty to auto-generate from timestamp.""" |
|
|
| data_exporter_frequency: int = 50 |
| """Data collection frequency (Hz) for the data exporter.""" |
|
|
| record_wrist_cameras: bool = False |
| """Record wrist camera streams (left_wrist, right_wrist) in the dataset.""" |
|
|
| text_to_speech: bool = True |
| """Enable voice feedback via espeak (data exporter).""" |
|
|
| |
| camera_viewer: bool = True |
| """Start the camera viewer pane.""" |
|
|
| camera_host: str = "localhost" |
| """Camera server host (shared by data exporter and viewer).""" |
|
|
| camera_port: int = 5555 |
| """Camera server port (shared by data exporter and viewer).""" |
|
|
|
|
| SESSION_NAME = "sonic_data_collection" |
|
|
|
|
| def _check_prerequisites(config: DataCollectionLaunchConfig): |
| """Verify that required tools and venvs exist.""" |
| errors = [] |
|
|
| if not shutil.which("tmux"): |
| errors.append("tmux is not installed. Install with: sudo apt install tmux") |
|
|
| repo_root = Path(__file__).resolve().parent.parent.parent |
|
|
| if not (repo_root / ".venv_teleop" / "bin" / "activate").exists(): |
| errors.append( |
| ".venv_teleop not found. Run: bash install_scripts/install_pico.sh" |
| ) |
|
|
| if not (repo_root / ".venv_data_collection" / "bin" / "activate").exists(): |
| errors.append( |
| ".venv_data_collection not found. Run: " |
| "bash install_scripts/install_data_collection.sh" |
| ) |
|
|
| deploy_dir = repo_root / "gear_sonic_deploy" |
| if not (deploy_dir / "deploy.sh").exists(): |
| errors.append( |
| f"gear_sonic_deploy/deploy.sh not found at {deploy_dir}. " |
| "Ensure the deploy directory is set up." |
| ) |
|
|
| if config.sim and not (repo_root / ".venv_sim" / "bin" / "activate").exists(): |
| errors.append( |
| ".venv_sim not found. Set up the simulation venv first " |
| "(see install instructions)." |
| ) |
|
|
| if config.pico_input_source not in {"xrt", "isaac-teleop"}: |
| errors.append("--pico-input-source must be one of: xrt, isaac-teleop") |
|
|
| if errors: |
| print("ERROR: Prerequisites not met:\n") |
| for e in errors: |
| print(f" - {e}") |
| print() |
| sys.exit(1) |
|
|
|
|
| def _kill_existing_session(): |
| """Kill any existing tmux session with our name.""" |
| subprocess.run( |
| ["tmux", "kill-session", "-t", SESSION_NAME], |
| capture_output=True, |
| ) |
|
|
|
|
| def _create_tmux_session(): |
| """Create a 4-pane tmux layout.""" |
| |
| subprocess.run( |
| ["tmux", "new-session", "-d", "-s", SESSION_NAME], |
| check=True, |
| ) |
|
|
| |
| subprocess.run( |
| ["tmux", "set-option", "-t", SESSION_NAME, "-g", "mouse", "on"], |
| ) |
|
|
| |
| subprocess.run( |
| ["tmux", "bind-key", "-T", "root", "C-\\", "kill-session"], |
| ) |
|
|
| |
| subprocess.run( |
| ["tmux", "rename-window", "-t", f"{SESSION_NAME}:0", "data_collection"], |
| ) |
|
|
| |
| |
| |
| |
|
|
| |
| subprocess.run( |
| ["tmux", "split-window", "-t", f"{SESSION_NAME}:0", "-h"], |
| ) |
|
|
| |
| subprocess.run( |
| ["tmux", "split-window", "-t", f"{SESSION_NAME}:0.0", "-v"], |
| ) |
|
|
| |
| subprocess.run( |
| ["tmux", "split-window", "-t", f"{SESSION_NAME}:0.2", "-v"], |
| ) |
|
|
| |
| time.sleep(5) |
|
|
|
|
| def _send_to_pane(pane_index: int, cmd: str, wait: float = 1.0): |
| """Send a command string to a tmux pane.""" |
| target = f"{SESSION_NAME}:0.{pane_index}" |
|
|
| subprocess.run( |
| ["tmux", "send-keys", "-t", target, cmd, "C-m"], |
| ) |
| time.sleep(wait) |
|
|
|
|
| def _check_pane_alive(pane_index: int) -> bool: |
| """Check if a tmux pane's process is still running.""" |
| target = f"{SESSION_NAME}:0.{pane_index}" |
| result = subprocess.run( |
| ["tmux", "list-panes", "-t", target, "-F", "#{pane_dead}"], |
| capture_output=True, |
| text=True, |
| ) |
| return result.stdout.strip() != "1" |
|
|
|
|
| def main(config: DataCollectionLaunchConfig): |
| repo_root = Path(__file__).resolve().parent.parent.parent |
|
|
| _check_prerequisites(config) |
| _kill_existing_session() |
|
|
| print("=" * 60) |
| print(" SONIC Data Collection Launcher") |
| print("=" * 60) |
| print(f" Mode: {'Simulation' if config.sim else 'Real Robot'}") |
| print(f" Task prompt: {config.task_prompt}") |
| print(f" Dataset name: {config.dataset_name or '(auto)'}") |
| print(f" Deploy input: {config.deploy_input_type}") |
| print(f" Teleop input: {config.pico_input_source}") |
| if config.deploy_checkpoint: |
| print(f" Checkpoint: {config.deploy_checkpoint}") |
| print(f" Camera: {config.camera_host}:{config.camera_port}") |
| print(f" DC frequency: {config.data_exporter_frequency} Hz") |
| print(f" Camera viewer: {'Yes' if config.camera_viewer else 'No'}") |
| print(f" Wrist cameras: {'Yes' if config.record_wrist_cameras else 'No'}") |
| print(f" Text-to-speech: {'Yes' if config.text_to_speech else 'No'}") |
| print(f" PC IP (for PICO): {_get_local_ip()}") |
| print(f" Teleop vis: vr3pt={config.pico_vis_vr3pt} smpl={config.pico_vis_smpl}") |
| print("=" * 60) |
|
|
| _create_tmux_session() |
| print(f"Created tmux session: {SESSION_NAME}") |
|
|
| |
| if config.sim: |
| subprocess.run( |
| ["tmux", "new-window", "-t", SESSION_NAME, "-n", "sim"], |
| ) |
| sim_cmd = ( |
| f"cd {repo_root} && " |
| f"source .venv_sim/bin/activate && " |
| f"python gear_sonic/scripts/run_sim_loop.py " |
| f"--enable-image-publish --enable-offscreen " |
| f"--camera-port {config.camera_port}" |
| ) |
| sim_target = f"{SESSION_NAME}:sim" |
| subprocess.run( |
| ["tmux", "send-keys", "-t", sim_target, sim_cmd, "C-m"], |
| ) |
| print("Starting MuJoCo simulator (window: sim)...") |
| time.sleep(3.0) |
|
|
| |
| subprocess.run( |
| ["tmux", "select-window", "-t", f"{SESSION_NAME}:data_collection"], |
| ) |
|
|
| |
| deploy_mode = "sim" if config.sim else "real" |
| deploy_cmd = ( |
| f"cd {repo_root / 'gear_sonic_deploy'} && " |
| f"./deploy.sh " |
| f"--input-type {config.deploy_input_type} " |
| f"--zmq-host {config.deploy_zmq_host} " |
| ) |
| if config.deploy_checkpoint: |
| deploy_cmd += f"--cp {config.deploy_checkpoint} " |
| if config.deploy_obs_config: |
| deploy_cmd += f"--obs-config {config.deploy_obs_config} " |
| if config.deploy_planner: |
| deploy_cmd += f"--planner {config.deploy_planner} " |
| if config.deploy_motion_data: |
| deploy_cmd += f"--motion-data {config.deploy_motion_data} " |
| if config.deploy_output_type: |
| deploy_cmd += f"--output-type {config.deploy_output_type} " |
| deploy_cmd += deploy_mode |
|
|
| print("Starting C++ deploy (pane 0)...") |
| _send_to_pane(0, deploy_cmd, wait=3.0) |
|
|
| if not _check_pane_alive(0): |
| print("WARNING: C++ deploy pane may have failed to start.") |
|
|
| |
| pico_cmd = ( |
| f"cd {repo_root} && " |
| f"source .venv_teleop/bin/activate && " |
| f"python gear_sonic/scripts/pico_manager_thread_server.py " |
| f"--input-source {config.pico_input_source}" |
| ) |
| if config.pico_manager: |
| pico_cmd += " --manager" |
| if config.pico_vis_vr3pt: |
| pico_cmd += " --vis_vr3pt" |
| if config.pico_vis_smpl: |
| pico_cmd += " --vis_smpl" |
| if config.pico_waist_tracking: |
| pico_cmd += " --waist_tracking" |
|
|
| print("Starting teleop streamer (pane 2)...") |
| _send_to_pane(1, pico_cmd, wait=2.0) |
|
|
| |
| if config.camera_viewer: |
| viewer_cmd = ( |
| f"cd {repo_root} && " |
| f"source .venv_data_collection/bin/activate && " |
| f"python gear_sonic/scripts/run_camera_viewer.py " |
| f"--camera-host {config.camera_host} " |
| f"--camera-port {config.camera_port}" |
| ) |
| print("Starting camera viewer (pane 3)...") |
| _send_to_pane(3, viewer_cmd, wait=2.0) |
|
|
| |
| exporter_cmd = ( |
| f"cd {repo_root} && " |
| f"source .venv_data_collection/bin/activate && " |
| f"python gear_sonic/scripts/run_data_exporter.py " |
| f"--task-prompt '{config.task_prompt}' " |
| f"--data-collection-frequency {config.data_exporter_frequency} " |
| f"--camera-host {config.camera_host} " |
| f"--camera-port {config.camera_port}" |
| ) |
| if config.dataset_name: |
| exporter_cmd += f" --dataset-name '{config.dataset_name}'" |
| if config.record_wrist_cameras: |
| exporter_cmd += " --record-wrist-cameras" |
| if not config.text_to_speech: |
| exporter_cmd += " --no-text-to-speech" |
|
|
| print("Starting data exporter (pane 1)...") |
| _send_to_pane(2, exporter_cmd, wait=1.0) |
|
|
| |
| subprocess.run( |
| ["tmux", "select-pane", "-t", f"{SESSION_NAME}:0.2"], |
| ) |
|
|
| print() |
| print("=" * 60) |
| print(" All components launched!") |
| print() |
| print(f" tmux session: {SESSION_NAME}") |
| print() |
| if config.sim: |
| print(" Window 'sim':") |
| print(" MuJoCo Simulator (.venv_sim)") |
| print() |
| print(" Window 'data_collection':") |
| print(" Pane 0 (top-left): C++ Deploy") |
| print(" Pane 1 (bottom-left): Teleop Streamer") |
| print(" Pane 2 (top-right): Data Exporter <-- you are here") |
| if config.camera_viewer: |
| print(" Pane 3 (bottom-right): Camera Viewer") |
| print() |
| print(" ** deploy.sh (pane 0) is waiting for confirmation —") |
| print(" click on pane 0 and press Enter to proceed **") |
| print() |
| print(" Controls:") |
| print(" Ctrl+b, arrow keys - Switch between panes") |
| if config.sim: |
| print(" Ctrl+b, n / p - Next / previous window") |
| print(" Ctrl+b, d - Detach from session") |
| print(" Ctrl+\\ - Kill entire session") |
| print("=" * 60) |
|
|
| |
| try: |
| subprocess.run(["tmux", "attach", "-t", SESSION_NAME]) |
| except KeyboardInterrupt: |
| pass |
|
|
| |
| result = subprocess.run( |
| ["tmux", "has-session", "-t", SESSION_NAME], |
| capture_output=True, |
| ) |
| if result.returncode == 0: |
| print(f"\nSession '{SESSION_NAME}' is still running.") |
| print(f" Reattach: tmux attach -t {SESSION_NAME}") |
| print(f" Kill: tmux kill-session -t {SESSION_NAME}") |
|
|
|
|
| def _signal_handler(sig, frame): |
| print("\nShutdown requested...") |
| subprocess.run( |
| ["tmux", "kill-session", "-t", SESSION_NAME], |
| capture_output=True, |
| ) |
| sys.exit(0) |
|
|
|
|
| if __name__ == "__main__": |
| signal.signal(signal.SIGINT, _signal_handler) |
| config = tyro.cli(DataCollectionLaunchConfig) |
| main(config) |
|
|