diff --git a/xr_rm_bringup/config/act_tomato_pick.yaml b/xr_rm_bringup/config/act_tomato_pick.yaml new file mode 100644 index 0000000..40e1067 --- /dev/null +++ b/xr_rm_bringup/config/act_tomato_pick.yaml @@ -0,0 +1,33 @@ +act_episode_recorder: + ros__parameters: + output_root: /home/robot/ACT_Data + task_name: tomato_pick + control_sample_topic: /xr_rm/right_rm75/act_control_sample + right_controller_topic: /xr/right_controller + left_controller_topic: /xr/left_controller + status_topic: /act/recording_status + + cam_high_serial: "234222303366" + cam_high_model: D455 + cam_right_wrist_serial: "412622272532" + cam_right_wrist_model: D405 + image_width: 640 + image_height: 480 + camera_fps: 30 + camera_warmup_sec: 5.0 + + control_rate_hz: 90.0 + sample_rate_hz: 30.0 + min_samples: 60 + max_samples: 1800 + min_control_hz: 27.0 + max_control_gap_ms: 100.0 + min_camera_fps: 27.0 + max_drop_ratio: 0.01 + max_feedback_age_ms: 50.0 + max_camera_age_ms: 50.0 + max_camera_skew_ms: 50.0 + min_free_space_gib: 4.0 + y_hold_sec: 1.0 + gripper_completion_timeout_sec: 3.0 + writer_queue_size: 8 diff --git a/xr_rm_bringup/launch/arm_debug.launch.py b/xr_rm_bringup/launch/arm_debug.launch.py index 663f871..19b89cb 100644 --- a/xr_rm_bringup/launch/arm_debug.launch.py +++ b/xr_rm_bringup/launch/arm_debug.launch.py @@ -80,6 +80,29 @@ def _validate_mujoco_mode(arm: str, use_mujoco: bool) -> None: raise ValueError("use_mujoco:=true requires arm:=both") +def _validate_act_mode( + arm: str, + use_mock: bool, + record_act: bool, +) -> None: + if record_act and (arm != "right" or use_mock): + raise ValueError( + "record_act:=true requires arm:=right use_mock:=false" + ) + + +def _act_recorder_node() -> Node: + """创建独立 ACT 数据采集节点;退出时不终止遥操作。""" + return Node( + package="xr_rm_teleop", + executable="act_episode_recorder", + name="act_episode_recorder", + output="screen", + prefix=[XR_PYTHON], + parameters=[_config_file("act_tomato_pick.yaml")], + ) + + def _single_arm_node( arm: str, use_mock: bool, @@ -164,10 +187,14 @@ def _launch_setup(context, *args, **kwargs): use_mujoco = _as_bool( LaunchConfiguration("use_mujoco").perform(context) ) + record_act = _as_bool( + LaunchConfiguration("record_act").perform(context) + ) if arm not in ("left", "right", "both"): raise ValueError("arm must be one of: left, right, both") _validate_mujoco_mode(arm, use_mujoco) + _validate_act_mode(arm, use_mock, record_act) nodes = [_udp_receiver_node()] if arm == "both": @@ -176,6 +203,8 @@ def _launch_setup(context, *args, **kwargs): nodes.append(_single_arm_node(arm, use_mock)) if use_mujoco: nodes.append(_mujoco_node()) + if record_act: + nodes.append(_act_recorder_node()) return nodes @@ -187,6 +216,8 @@ def generate_launch_description() -> LaunchDescription: DeclareLaunchArgument("use_mock", default_value="true"), # true 时额外启动只读 MuJoCo 双臂显示,默认不改变现有启动行为。 DeclareLaunchArgument("use_mujoco", default_value="false"), + # true 时只允许右臂真机,并启动独立 ACT 数据采集节点。 + DeclareLaunchArgument("record_act", default_value="false"), # UDP 监听参数,需要与 PICO 端或 sample_udp_sender 保持一致。 DeclareLaunchArgument("udp_host", default_value="0.0.0.0"), DeclareLaunchArgument("udp_port", default_value="15000"), diff --git a/xr_rm_bringup/test/test_arm_debug_launch.py b/xr_rm_bringup/test/test_arm_debug_launch.py index a5f3e1e..be2e3d3 100644 --- a/xr_rm_bringup/test/test_arm_debug_launch.py +++ b/xr_rm_bringup/test/test_arm_debug_launch.py @@ -41,3 +41,37 @@ def test_udp_receiver_exit_shuts_down_launch() -> None: receiver = arm_debug_launch._udp_receiver_node() assert isinstance(receiver._ExecuteLocal__on_exit, Shutdown) + + +def test_act_recording_is_disabled_by_default() -> None: + description = arm_debug_launch.generate_launch_description() + arguments = { + entity.name: entity + for entity in description.entities + if isinstance(entity, DeclareLaunchArgument) + } + + assert perform_substitutions( + LaunchContext(), + arguments["record_act"].default_value, + ) == "false" + + +@pytest.mark.parametrize( + ("arm", "use_mock"), + [("left", False), ("both", False), ("right", True)], +) +def test_act_recording_rejects_unsupported_modes(arm, use_mock) -> None: + with pytest.raises(ValueError, match="arm:=right use_mock:=false"): + arm_debug_launch._validate_act_mode(arm, use_mock, True) + + +def test_act_recording_accepts_right_real_mode() -> None: + arm_debug_launch._validate_act_mode("right", False, True) + arm_debug_launch._validate_act_mode("both", True, False) + + +def test_act_recorder_exit_does_not_shutdown_teleoperation() -> None: + recorder = arm_debug_launch._act_recorder_node() + + assert recorder._ExecuteLocal__on_exit is None