diff --git a/README.md b/README.md index 5fe0e81..9554b47 100755 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ source install/setup.bash 真机模式还需要安装睿尔曼 Python API2。若未安装,mock 模式仍可正常使用;真机启动时会提示缺少 `Robotic_Arm` 包。 -遥操作节点固定由 `/home/robot/miniconda3/envs/xr/bin/python` 启动,并复用其中的 Python 3.10、Placo 0.9.4、Pinocchio 3.7.0 和 NumPy 2.2.6。`ros2`、`colcon` 和 `udp_controller_receiver` 仍使用系统 Python。禁止通过 `pip --user`、`sudo pip` 或系统安装升级 Placo、Pinocchio、EigenPy 和 NumPy。 +遥操作和 MuJoCo 节点固定由 `/home/robot/miniconda3/envs/xr/bin/python` 启动,并复用其中的 Python 3.10、Placo 0.9.4、Pinocchio 3.7.0、NumPy 2.2.6 和 MuJoCo 3.10.0。`ros2`、`colcon`、pytest 和 `udp_controller_receiver` 仍使用系统 Python。禁止通过 `pip --user`、`sudo pip` 或系统安装升级 Placo、Pinocchio、EigenPy 和 NumPy。 只读检查 Placo 版本: @@ -115,6 +115,20 @@ source install/setup.bash 输出必须为 `0.9.4`。 +同时检查 MuJoCo: + +```bash +/home/robot/miniconda3/envs/xr/bin/python -c \ + "import mujoco; print(mujoco.__version__)" +``` + +当前验证版本为 `3.10.0`。系统 pytest 会通过 `xr_rm_mujoco/test/conftest.py` 复用该固定 XR 环境中的 MuJoCo,因此新包可直接按 ROS2 标准方式测试: + +```bash +colcon test --packages-select xr_rm_mujoco --event-handlers console_direct+ +colcon test-result --verbose +``` + 如果希望 `launcher_ui.py` 从任意目录找到工作空间,可以设置: ```bash diff --git a/xr_rm_mujoco/test/conftest.py b/xr_rm_mujoco/test/conftest.py new file mode 100644 index 0000000..359c7ac --- /dev/null +++ b/xr_rm_mujoco/test/conftest.py @@ -0,0 +1,11 @@ +"""让 ROS2 系统 pytest 复用项目固定 XR 环境中的 MuJoCo。""" + +import sys +from pathlib import Path + + +XR_SITE_PACKAGES = Path( + "/home/robot/miniconda3/envs/xr/lib/python3.10/site-packages" +) +if XR_SITE_PACKAGES.is_dir(): + sys.path.append(str(XR_SITE_PACKAGES)) diff --git a/xr_rm_mujoco/test/test_dual_arm_simulator.py b/xr_rm_mujoco/test/test_dual_arm_simulator.py index 8e27dd2..4aefc0e 100644 --- a/xr_rm_mujoco/test/test_dual_arm_simulator.py +++ b/xr_rm_mujoco/test/test_dual_arm_simulator.py @@ -80,7 +80,8 @@ def test_main_closes_viewer_cleanly_on_keyboard_interrupt(monkeypatch) -> None: node.close_viewer = lambda: close_viewer(node) init = Mock() spin = Mock(side_effect=KeyboardInterrupt) - sleep = Mock() + sleep = Mock(side_effect=[KeyboardInterrupt, None]) + monotonic = Mock(side_effect=[0.0, 0.0, 0.04]) shutdown = Mock() monkeypatch.setattr(simulator_module, "DualArmSimulator", lambda: node) @@ -88,6 +89,7 @@ def test_main_closes_viewer_cleanly_on_keyboard_interrupt(monkeypatch) -> None: monkeypatch.setattr(simulator_module.rclpy, "spin", spin) monkeypatch.setattr(simulator_module.rclpy, "ok", lambda: True) monkeypatch.setattr(simulator_module.time, "sleep", sleep) + monkeypatch.setattr(simulator_module.time, "monotonic", monotonic) monkeypatch.setattr(simulator_module.rclpy, "shutdown", shutdown) simulator_module.main(["--test"]) @@ -95,7 +97,9 @@ def test_main_closes_viewer_cleanly_on_keyboard_interrupt(monkeypatch) -> None: init.assert_called_once_with(args=["--test"]) spin.assert_called_once_with(node) viewer.close.assert_called_once_with() - sleep.assert_called_once_with(0.1) + assert [call.args[0] for call in sleep.call_args_list] == pytest.approx( + [0.1, 0.06] + ) node.destroy_node.assert_called_once_with() shutdown.assert_called_once_with() assert node._viewer is None diff --git a/xr_rm_mujoco/xr_rm_mujoco/dual_arm_simulator.py b/xr_rm_mujoco/xr_rm_mujoco/dual_arm_simulator.py index 893b0e9..ca338d4 100644 --- a/xr_rm_mujoco/xr_rm_mujoco/dual_arm_simulator.py +++ b/xr_rm_mujoco/xr_rm_mujoco/dual_arm_simulator.py @@ -181,7 +181,16 @@ class DualArmSimulator(Node): if self._viewer is not None: self._viewer.close() # MuJoCo 在后台 daemon 线程释放 GLX;立即退出解释器会触发段错误。 - time.sleep(0.1) + deadline = time.monotonic() + 0.1 + while True: + remaining = deadline - time.monotonic() + if remaining <= 0.0: + break + try: + time.sleep(remaining) + break + except KeyboardInterrupt: + continue self._viewer = None