P5c-3 仿真启动器跑通:Gazebo + spawn + 两个控制器 active

端到端验收(headless,本机无显示器):
    ign 加载 Physics/UserCommands/SceneBroadcaster/Sensors + gz_ros2_control
    机械臂 spawn 进 assembly 世界,7 个 sim_joint 全在 /joint_states
    joint_state_broadcaster 与 rizon_arm_controller 双双 active
    world → sim_tcp 可查,且末端位姿 3 秒内不变(重力下站住)
    网格解析报错 0 条

新增:
  scripts/run_sim_gz.sh                  此前**不存在**,却被 install_sim_env.sh
                                         与 build_sim.sh 的提示语引用
  arm_sim_gazebo/robot_description.py    展开 + 归一化,launch 与 CLI 共用一份
  arm_sim_gazebo/gazebo_sim_launcher.py  薄壳:准备描述后转发给 launch 文件
  launch/gazebo_sim.launch.py            起 gz → 发归一化 URDF → spawn → 拉控制器
  test/test_robot_description.py         含"仓库里这份 world 必须过守卫"的测试

接线过程中实测踩到并修掉的三个坑,共同特征是**每一步看起来都成功**:

1) urdf_normalize 在 Python 路径下静默失效(我自己引入的,最严重)
   xacro 的输出格式在不同环境里不一样:真机环境的 CLI 缩进成多行,而仿真环境
   走 xacro.process_file().toxml() 时全挤在一行。归一化的正则要求标签之间必须有
   \n,于是**仿真路径下它什么都没做**(报告"包裹 0 处"),而 CLI 路径下正常 ——
   之前那次"已修好"的验证走的就是 CLI,所以看起来过了。
   后果是 Gazebo 会丢掉整条机械臂,正是这个归一化本来要防的事。
   已改成对空白格式零假设,并加回归测试锁住。

2) world 里声明了 <plugin> 会把默认系统挤掉
   只要 world 声明任何一个 <plugin>,Gazebo 就不再加载 server.config 的
   Physics/UserCommands/SceneBroadcaster。缺 UserCommands →
   /world/<名>/create 服务不存在 → ros_gz_sim create 调服务超时后回退发 topic、
   没人收 → **模型从未进入 Gazebo**,而 create 节点仍打印
   "OK creation of entity",控制器则永远卡在 "waiting for service"。
   已在这三个系统在 assembly.world 里显式声明,并加纯文本守卫
   check_base_plugins(),让"加了插件忘了补基础系统"在启动前就被指出。

3) gz 的插件与资源搜索路径缺 $CONDA_PREFIX/lib
   gz_ros2_control 报 "Failed to load system plugin
   [libgz_ros2_control-system.so] : couldn't find shared library",而 ldd 全齐、
   库就在 $CONDA_PREFIX/lib 下。原因是 gz 的 SystemLoader 先在搜索路径里找文件,
   找不到就直接放弃,不去 dlopen 裸文件名(所以 conda 二进制的 RPATH 兜不住);
   而 ros_gz_sim 只把 LD_LIBRARY_PATH 拼进搜索路径,conda 激活并不往那里加。
   同一原因导致 mesh 的 model:// 也解析不了(视觉报错很吵,但更要紧的是
   **碰撞网格也加载不出来**,会让后续装配的接触判据失效)。
   由于 gz_sim.launch.py 用 `from os import environ` 在生成 launch 描述时就读
   进程环境,这些变量**必须在 ros2 launch 之前**设好,因此放在 sim_env.sh ——
   在 launch 文件里用 SetEnvironmentVariable 补是无效的,会被它自己算的那份
   字典覆盖。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
arm_dev
2026-09-23 14:48:16 +08:00
parent e9da641160
commit 01940f54e7
11 changed files with 1253 additions and 37 deletions
+20 -6
View File
@@ -1,7 +1,21 @@
import os
from glob import glob
from setuptools import find_packages, setup
package_name = 'arm_sim_gazebo'
def _existing(paths):
"""过滤掉不存在的路径。
glob() 会把**悬空符号链接**也列出来,而 colcon 在拷贝阶段会因此直接失败
("can't copy ... doesn't exist or not a regular file")。
arm_application 的 setup.py 就因这个问题挂过一次构建,这里从一开始就防住。
"""
return [p for p in paths if os.path.isfile(p)]
setup(
name=package_name,
version='0.1.0',
@@ -9,6 +23,8 @@ setup(
data_files=[
('share/ament_index/resource_index/packages', ['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
# launch 文件必须显式安装,否则 ros2 launch arm_sim_gazebo ... 找不到
(os.path.join('share', package_name, 'launch'), _existing(glob('launch/*.py'))),
],
install_requires=['setuptools', 'PyYAML'],
zip_safe=True,
@@ -18,15 +34,13 @@ setup(
license='Apache-2.0',
entry_points={
'console_scripts': [
# devices/sim/gazebo/manifest.yaml 里声明的那个可执行名,别改。
# 它只做"准备描述 + 转发给 launch 文件",逻辑在
# arm_sim_gazebo/gazebo_sim_launcher.py。
'gazebo_sim_launcher = arm_sim_gazebo.gazebo_sim_launcher:main',
# 独立小工具:只看归一化做了什么,不启动 Gazebo。
# 排查"为什么 Gazebo 说我的 link 不存在"时非常省时间。
'urdf_normalize = arm_sim_gazebo.urdf_normalize:main',
# TODO(P5c-3): manifest 声明的启动器
# devices/sim/gazebo/manifest.yaml -> driver.executable: gazebo_sim_launcher
# 尚未实现,所以这里**先不注册** —— 注册了但模块不存在,
# 构建能过、一运行就 ImportError,不如老实缺着。
# P5c-3 会补上 gazebo_sim_launcher.py 并在此注册。
# 'gazebo_sim_launcher = arm_sim_gazebo.gazebo_sim_launcher:main',
],
},
)