| from pathlib import Path |
| import signal |
| import subprocess |
| import sys |
| import time |
|
|
| import tyro |
|
|
| from decoupled_wbc.control.main.teleop.configs.configs import DeploymentConfig |
| from decoupled_wbc.control.utils.run_real_checklist import show_deployment_checklist |
|
|
|
|
| class G1Deployment: |
| """ |
| Unified deployment manager for G1 robot with one-click operation. |
| Handles camera setup, control loop, teleoperation, and data collection. |
| Uses tmux for process management and I/O handling. |
| """ |
|
|
| def __init__(self, config: DeploymentConfig): |
| self.config = config |
|
|
| |
| self.project_root = Path(__file__).resolve().parent.parent |
|
|
| |
| self.session_name = "g1_deployment" |
|
|
| |
| self._create_tmux_session() |
|
|
| def _create_tmux_session(self): |
| """Create a new tmux session if it doesn't exist""" |
| |
| result = subprocess.run( |
| ["tmux", "has-session", "-t", self.session_name], capture_output=True, text=True |
| ) |
|
|
| if result.returncode != 0: |
| |
| subprocess.run(["tmux", "new-session", "-d", "-s", self.session_name]) |
| print(f"Created new tmux session: {self.session_name}") |
|
|
| |
| |
| subprocess.run( |
| ["tmux", "rename-window", "-t", f"{self.session_name}:0", "control_data_teleop"] |
| ) |
| |
| subprocess.run(["tmux", "split-window", "-t", f"{self.session_name}:0", "-h"]) |
| |
| subprocess.run(["tmux", "split-window", "-t", f"{self.session_name}:0.1", "-v"]) |
| |
| subprocess.run(["tmux", "select-pane", "-t", f"{self.session_name}:0.0"]) |
|
|
| def _run_in_tmux(self, name, cmd, wait_time=2, pane_index=None): |
| """Run a command in a new tmux window or pane""" |
| if pane_index is not None: |
| |
| target = f"{self.session_name}:0.{pane_index}" |
| else: |
| |
| subprocess.run(["tmux", "new-window", "-t", self.session_name, "-n", name]) |
| target = f"{self.session_name}:{name}" |
|
|
| |
| trap_cmd = f"trap 'tmux kill-session -t {self.session_name}' QUIT" |
|
|
| |
| env_cmd = f"export DECOUPLED_WBC_TMUX_SESSION={self.session_name}" |
|
|
| |
| cmd_str = " ".join(str(x) for x in cmd) |
| full_cmd = f"{trap_cmd}; {env_cmd}; {cmd_str}" |
|
|
| |
| subprocess.run(["tmux", "send-keys", "-t", target, full_cmd, "C-m"]) |
|
|
| |
| time.sleep(wait_time) |
|
|
| |
| result = subprocess.run( |
| ["tmux", "list-panes", "-t", target, "-F", "#{pane_dead}"], |
| capture_output=True, |
| text=True, |
| ) |
|
|
| if result.stdout.strip() == "1": |
| print(f"ERROR: {name} failed to start!") |
| return False |
|
|
| return True |
|
|
| def start_camera_sensor(self): |
| """Start the camera sensor in local mode if we are using replay video""" |
| if self.config.egoview_replay_dummy is None and self.config.head_replay_dummy is None: |
| return |
|
|
| print("Starting camera sensor in local mode...") |
| cmd = [ |
| sys.executable, |
| str(self.project_root / "control/sensor/composed_camera.py"), |
| "--egoview_camera", |
| self.config.egoview_replay_dummy, |
| "--head_camera", |
| self.config.head_replay_dummy, |
| "--port", |
| str(self.config.camera_port), |
| "--host", |
| "localhost", |
| ] |
|
|
| if not self._run_in_tmux("camera_sensor", cmd): |
| print("ERROR: Camera sensor failed to start!") |
| print("Continuing without camera sensor...") |
| else: |
| print("Camera sensor started successfully.") |
|
|
| def start_camera_viewer(self): |
| """Start the ROS rqt camera viewer""" |
| if not self.config.view_camera: |
| return |
|
|
| print("Starting camera viewer...") |
| |
| cmd = [ |
| sys.executable, |
| str(self.project_root / "control/main/teleop/run_camera_viewer.py"), |
| "--camera_host", |
| self.config.camera_host, |
| "--camera_port", |
| str(self.config.camera_port), |
| "--fps", |
| str(self.config.fps), |
| ] |
|
|
| if not self._run_in_tmux("camera_viewer", cmd): |
| print("ERROR: Camera viewer failed to start!") |
| print("Continuing without camera viewer...") |
| else: |
| print("Camera viewer started successfully.") |
|
|
| def start_sim_loop(self): |
| """Start the simulation loop in a separate process""" |
| print("Starting simulation loop...") |
| cmd = [ |
| sys.executable, |
| str(self.project_root / "control/main/teleop/run_sim_loop.py"), |
| "--wbc_version", |
| self.config.wbc_version, |
| "--interface", |
| self.config.interface, |
| "--simulator", |
| self.config.simulator, |
| "--sim_frequency", |
| str(self.config.sim_frequency), |
| "--env_name", |
| self.config.env_name, |
| "--camera_port", |
| str(self.config.camera_port), |
| ] |
|
|
| |
| if self.config.enable_waist: |
| cmd.append("--enable_waist") |
| else: |
| cmd.append("--no-enable_waist") |
|
|
| if self.config.with_hands: |
| cmd.append("--with_hands") |
| else: |
| cmd.append("--no-with_hands") |
|
|
| if self.config.image_publish: |
| cmd.append("--enable_image_publish") |
| cmd.append("--enable_offscreen") |
| else: |
| cmd.append("--no-enable_image_publish") |
|
|
| if self.config.enable_onscreen: |
| cmd.append("--enable_onscreen") |
| else: |
| cmd.append("--no-enable_onscreen") |
|
|
| if not self._run_in_tmux("sim_loop", cmd, wait_time=5): |
| print("ERROR: Simulation loop failed to start!") |
| self.cleanup() |
| sys.exit(1) |
|
|
| print("Simulation loop started successfully. Waiting for warmup for 10 seconds...") |
| time.sleep(10) |
|
|
| def start_control_loop(self): |
| """Start the G1 control loop""" |
| print("Starting G1 control loop...") |
| cmd = [ |
| sys.executable, |
| str(self.project_root / "control/main/teleop/run_g1_control_loop.py"), |
| "--wbc_version", |
| self.config.wbc_version, |
| "--wbc_model_path", |
| self.config.wbc_model_path, |
| "--wbc_policy_class", |
| self.config.wbc_policy_class, |
| "--interface", |
| self.config.interface, |
| "--simulator", |
| "None" if self.config.sim_in_single_process else self.config.simulator, |
| "--control_frequency", |
| str(self.config.control_frequency), |
| ] |
|
|
| |
| if self.config.enable_waist: |
| cmd.append("--enable_waist") |
| else: |
| cmd.append("--no-enable_waist") |
|
|
| if self.config.with_hands: |
| cmd.append("--with_hands") |
| else: |
| cmd.append("--no-with_hands") |
|
|
| if self.config.high_elbow_pose: |
| cmd.append("--high_elbow_pose") |
| else: |
| cmd.append("--no-high_elbow_pose") |
|
|
| |
| |
| |
| if self.config.enable_gravity_compensation: |
| cmd.append("--enable_gravity_compensation") |
| |
| if self.config.gravity_compensation_joints: |
| cmd.extend( |
| ["--gravity_compensation_joints"] + self.config.gravity_compensation_joints |
| ) |
| else: |
| cmd.append("--no-enable_gravity_compensation") |
|
|
| if not self._run_in_tmux("control", cmd, wait_time=3, pane_index=0): |
| print("ERROR: Control loop failed to start!") |
| self.cleanup() |
| sys.exit(1) |
|
|
| print("Control loop started successfully.") |
| print("Controls: 'i' for initial pose, ']' to activate locomotion") |
|
|
| def start_policy(self): |
| """Start either teleop or inference policy based on configuration""" |
| if not self.config.enable_upper_body_operation: |
| print("Upper body operation disabled in config.") |
| return |
|
|
| self.start_teleop() |
|
|
| def start_teleop(self): |
| """Start the teleoperation policy""" |
| print("Starting teleoperation policy...") |
| cmd = [ |
| sys.executable, |
| str(self.project_root / "control/main/teleop/run_teleop_policy_loop.py"), |
| "--body_control_device", |
| self.config.body_control_device, |
| "--hand_control_device", |
| self.config.hand_control_device, |
| "--body_streamer_ip", |
| self.config.body_streamer_ip, |
| "--body_streamer_keyword", |
| self.config.body_streamer_keyword, |
| ] |
|
|
| |
| if self.config.enable_waist: |
| cmd.append("--enable_waist") |
| else: |
| cmd.append("--no-enable_waist") |
|
|
| if self.config.high_elbow_pose: |
| cmd.append("--high_elbow_pose") |
| else: |
| cmd.append("--no-high_elbow_pose") |
|
|
| if self.config.enable_visualization: |
| cmd.append("--enable_visualization") |
| else: |
| cmd.append("--no-enable_visualization") |
|
|
| if self.config.enable_real_device: |
| cmd.append("--enable_real_device") |
| else: |
| cmd.append("--no-enable_real_device") |
|
|
| if not self._run_in_tmux("teleop", cmd, pane_index=2): |
| print("ERROR: Teleoperation policy failed to start!") |
| print("Continuing without teleoperation...") |
| else: |
| print("Teleoperation policy started successfully.") |
| print("Press 'l' in the control loop terminal to start teleoperation.") |
|
|
| def start_data_collection(self): |
| """Start the data collection process""" |
| if not self.config.data_collection: |
| print("Data collection disabled in config.") |
| return |
|
|
| print("Starting data collection...") |
| cmd = [ |
| sys.executable, |
| str(self.project_root / "control/main/teleop/run_g1_data_exporter.py"), |
| "--data_collection_frequency", |
| str(self.config.data_collection_frequency), |
| "--root_output_dir", |
| self.config.root_output_dir, |
| "--lower_body_policy", |
| self.config.wbc_version, |
| "--wbc_model_path", |
| self.config.wbc_model_path, |
| "--camera_host", |
| self.config.camera_host, |
| "--camera_port", |
| str(self.config.camera_port), |
| ] |
|
|
| if not self._run_in_tmux("data", cmd, pane_index=1): |
| print("ERROR: Data collection failed to start!") |
| print("Continuing without data collection...") |
| else: |
| print("Data collection started successfully.") |
| print("Press 'c' in the control loop terminal to start/stop recording data.") |
|
|
| def start_webcam_recording(self): |
| """Start webcam recording for real robot deployment monitoring""" |
| if not self.config.enable_webcam_recording or self.config.env_type != "real": |
| return |
|
|
| print("Starting webcam recording for deployment monitoring...") |
| cmd = [ |
| sys.executable, |
| str(self.project_root / "scripts/run_webcam_recorder.py"), |
| "--output_dir", |
| self.config.webcam_output_dir, |
| ] |
|
|
| if not self._run_in_tmux("webcam", cmd): |
| print("ERROR: Webcam recording failed to start!") |
| print("Continuing without webcam recording...") |
| else: |
| print("Webcam recording started successfully.") |
| print("External camera recording deployment activities to logs_experiment/") |
|
|
| def deploy(self): |
| """ |
| Run the complete deployment process |
| """ |
| print("Starting G1 deployment with config:") |
| print(f" Robot IP: {self.config.robot_ip}") |
| print(f" WBC Version: {self.config.wbc_version}") |
| print(f" Interface: {self.config.interface}") |
| print(f" Policy Mode: {self.config.upper_body_operation_mode}") |
| print(f" With Hands: {self.config.with_hands}") |
| print(f" View Camera: {self.config.view_camera}") |
| print(f" Enable Waist: {self.config.enable_waist}") |
| print(f" High Elbow Pose: {self.config.high_elbow_pose}") |
| print(f" Gravity Compensation: {self.config.enable_gravity_compensation}") |
| if self.config.enable_gravity_compensation: |
| print(f" Gravity Comp Joints: {self.config.gravity_compensation_joints}") |
| print( |
| f" Webcam Recording: {self.config.enable_webcam_recording and self.config.env_type == 'real'}" |
| ) |
| print(f" Sim in Single Process: {self.config.sim_in_single_process}") |
| if self.config.sim_in_single_process: |
| print(f" Image Publish: {self.config.image_publish}") |
|
|
| |
| if self.config.env_type == "real": |
| if not show_deployment_checklist(): |
| sys.exit(1) |
|
|
| |
| signal.signal(signal.SIGINT, self.signal_handler) |
|
|
| |
| |
| if self.config.sim_in_single_process: |
| self.start_sim_loop() |
|
|
| self.start_control_loop() |
| self.start_camera_viewer() |
| self.start_policy() |
| self.start_data_collection() |
| self.start_webcam_recording() |
|
|
| print("\n--- G1 DEPLOYMENT COMPLETE ---") |
| print("All systems running in tmux session:", self.session_name) |
| print("Press Ctrl+b then d to detach from the session") |
| print("Press Ctrl+\\ in any window to shutdown all components.") |
|
|
| try: |
| |
| subprocess.run( |
| [ |
| "tmux", |
| "attach", |
| "-t", |
| self.session_name, |
| ";", |
| "select-window", |
| "-t", |
| "control_data_teleop", |
| ] |
| ) |
| except KeyboardInterrupt: |
| print("\nShutdown requested...") |
| self.cleanup() |
| sys.exit(0) |
|
|
| |
| try: |
| while True: |
| |
| result = subprocess.run( |
| ["tmux", "has-session", "-t", self.session_name], capture_output=True, text=True |
| ) |
|
|
| if result.returncode != 0: |
| print("Tmux session terminated. Exiting.") |
| break |
|
|
| time.sleep(1) |
| except KeyboardInterrupt: |
| print("\nShutdown requested...") |
| finally: |
| self.cleanup() |
|
|
| def cleanup(self): |
| """Clean up tmux session""" |
| print("Cleaning up tmux session...") |
| try: |
| |
| subprocess.run(["tmux", "kill-session", "-t", self.session_name], timeout=5) |
| print("Tmux session terminated successfully.") |
| except subprocess.TimeoutExpired: |
| print("Warning: Tmux session termination timed out, forcing kill...") |
| subprocess.run(["tmux", "kill-session", "-t", self.session_name, "-9"]) |
| except Exception as e: |
| print(f"Warning: Error during cleanup: {e}") |
| print("Cleanup complete.") |
|
|
| def signal_handler(self, sig, frame): |
| """Handle SIGINT (Ctrl+C) gracefully""" |
| print("\nShutdown signal received...") |
| self.cleanup() |
| sys.exit(0) |
|
|
|
|
| def main(): |
| """Main entry point with automatic CLI generation from G1Config dataclass""" |
| |
| config = tyro.cli(DeploymentConfig) |
|
|
| |
| deployment = G1Deployment(config) |
| deployment.deploy() |
|
|
|
|
| if __name__ == "__main__": |
| |
|
|
| main() |
|
|