"""单臂/双臂通用调试入口。 该 launch 文件用于现场调试阶段按需启动左臂、右臂或双臂,并可通过 `use_mock` 在 mock 模式和 RM75 真机模式之间切换。它会固定启动 UDP 手柄接收节点,再根据 `arm:=left|right|both` 选择对应的遥操作节点。 """ from pathlib import Path from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, OpaqueFunction from launch.substitutions import LaunchConfiguration, PathJoinSubstitution from launch_ros.actions import Node from launch_ros.substitutions import FindPackageShare XR_PYTHON = "/home/robot/miniconda3/envs/xr/bin/python" def _as_bool(value: str) -> bool: """把 launch 字符串参数转换成 Python bool,便于在 OpaqueFunction 中分支。""" return value.strip().lower() in ("1", "true", "yes", "on") def _config_file(name: str) -> PathJoinSubstitution: """生成 xr_rm_bringup/config 下配置文件的可安装路径。""" return PathJoinSubstitution([ FindPackageShare("xr_rm_bringup"), "config", name, ]) def _rm75_urdf() -> PathJoinSubstitution: return PathJoinSubstitution([ FindPackageShare("xr_rm_teleop"), "models", "rm75_omnipicker", "urdf", "RM75-B_OmniPicker_fixed.urdf", ]) def _udp_receiver_node() -> Node: """接收 PICO/XR UDP 数据,并发布左右手柄 ROS2 话题。""" return Node( package="xr_rm_input", executable="udp_controller_receiver", name="udp_controller_receiver", output="screen", parameters=[{ "udp_host": LaunchConfiguration("udp_host"), "udp_port": LaunchConfiguration("udp_port"), "timer_hz": LaunchConfiguration("udp_timer_hz"), "left_topic": "/xr/left_controller", "right_topic": "/xr/right_controller", }], ) def _single_arm_node( arm: str, use_mock: bool, ) -> Node: """创建单臂调试节点;左/右臂分别使用独立 YAML,节点名保持单臂默认名。""" config_name = "left_arm_rm75.yaml" if arm == "left" else "right_arm_rm75.yaml" arm_name = _arm_name(arm) return Node( package="xr_rm_teleop", executable="single_arm_velocity_teleop", name="single_arm_velocity_teleop", output="screen", prefix=[XR_PYTHON], parameters=[ _config_file(config_name), { "use_mock": use_mock, "robot_urdf_path": _rm75_urdf(), "peripheral_config_file": _config_file("peripherals_rm75.yaml"), "peripheral_arm": arm, "tool_command_topic": f"/xr_rm/{arm_name}/tool_enable", }, ], ) def _arm_name(arm: str) -> str: return "left_rm75" if arm == "left" else "right_rm75" def _dual_arm_nodes(use_mock: bool) -> list[Node]: """创建双臂节点;两个节点共用双臂 YAML,但节点名区分左右臂参数命名空间。""" config_file = _config_file("dual_arm_rm75.yaml") return [ Node( package="xr_rm_teleop", executable="single_arm_velocity_teleop", name="left_arm_teleop", output="screen", prefix=[XR_PYTHON], parameters=[ config_file, { "use_mock": use_mock, "robot_urdf_path": _rm75_urdf(), "peripheral_config_file": _config_file("peripherals_rm75.yaml"), "peripheral_arm": "left", "tool_command_topic": "/xr_rm/left_rm75/tool_enable", }, ], ), Node( package="xr_rm_teleop", executable="single_arm_velocity_teleop", name="right_arm_teleop", output="screen", prefix=[XR_PYTHON], parameters=[ config_file, { "use_mock": use_mock, "robot_urdf_path": _rm75_urdf(), "peripheral_config_file": _config_file("peripherals_rm75.yaml"), "peripheral_arm": "right", "tool_command_topic": "/xr_rm/right_rm75/tool_enable", }, ], ), ] def _launch_setup(context, *args, **kwargs): """运行时读取 launch 参数,决定启动单臂还是双臂。""" del args, kwargs if not Path(XR_PYTHON).is_file(): raise RuntimeError( f"XR Python not found: {XR_PYTHON}; " "Placo 0.9.4 must not be installed globally" ) arm = LaunchConfiguration("arm").perform(context).strip().lower() use_mock = _as_bool(LaunchConfiguration("use_mock").perform(context)) if arm not in ("left", "right", "both"): raise ValueError("arm must be one of: left, right, both") nodes = [_udp_receiver_node()] if arm == "both": nodes.extend(_dual_arm_nodes(use_mock)) else: nodes.append(_single_arm_node(arm, use_mock)) return nodes def generate_launch_description() -> LaunchDescription: return LaunchDescription([ # 调试目标:left/right/both;默认右臂方便单臂逐步上真机。 DeclareLaunchArgument("arm", default_value="right"), # true 时只跑 mock,不连接 RM75;false 时通过 RealMan SDK 连接真机。 DeclareLaunchArgument("use_mock", default_value="true"), # UDP 监听参数,需要与 PICO 端或 sample_udp_sender 保持一致。 DeclareLaunchArgument("udp_host", default_value="0.0.0.0"), DeclareLaunchArgument("udp_port", default_value="15000"), # UDP receiver 轮询频率高于 PICO 发送频率,减少 socket 中等待时间。 DeclareLaunchArgument("udp_timer_hz", default_value="200.0"), # OpaqueFunction 允许根据 arm/use_mock 等运行时参数动态生成节点。 OpaqueFunction(function=_launch_setup), ])