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:
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 启动 Gazebo 仿真。
|
||||
#
|
||||
# 用法:
|
||||
# bash scripts/run_sim_gz.sh # 无头(无显示器环境用这个)
|
||||
# bash scripts/run_sim_gz.sh --gui # 带 Gazebo GUI
|
||||
# bash scripts/run_sim_gz.sh --check # 只自检描述,不启动任何东西
|
||||
# bash scripts/run_sim_gz.sh --print-urdf # 打印归一化后的 URDF
|
||||
# bash scripts/run_sim_gz.sh --dry-run # 只打印将执行的命令
|
||||
# bash scripts/run_sim_gz.sh --verbose # 打开 Gazebo 详细日志
|
||||
# bash scripts/run_sim_gz.sh --world /abs/other.world
|
||||
# bash scripts/run_sim_gz.sh --no-gripper --no-sensors # 最省资源的运动学验证
|
||||
#
|
||||
# 与 scripts/run_sim.sh 的区别(两个名字容易混,这里说清):
|
||||
# run_sim.sh ← 走 arm_bringup 的 **mock** 驱动:假数据 + RViz,无物理
|
||||
# run_sim_gz.sh ← 走 Gazebo:真物理、真接触
|
||||
# 两者互不影响,也都不需要真机。要验证力控/接触类逻辑只能用后者。
|
||||
#
|
||||
# ⚠️ 这个脚本此前**不存在**,却被 install_sim_env.sh(两处)与 build_sim.sh 的
|
||||
# 提示语引用 —— 照做会直接报"文件不存在"。现在补上。
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT}"
|
||||
|
||||
# 显式映射到 gazebo_sim_launcher 的选项,不做原样透传。
|
||||
# 透传会把拼错的选项静默吞掉(ros2 launch 对未知的 key:=value 不报错),
|
||||
# 而"我明明设了 --gui 却没生效"这类问题极难查。
|
||||
LAUNCH_ARGS=()
|
||||
PASS_ONLY=0 # 1 = 只准备描述,不启动(--check / --print-urdf / --dry-run)
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--gui) LAUNCH_ARGS+=(--headless false); shift ;;
|
||||
--headless) LAUNCH_ARGS+=(--headless true); shift ;;
|
||||
--verbose|-v) LAUNCH_ARGS+=(--verbose); shift ;;
|
||||
--no-gripper) LAUNCH_ARGS+=(--no-gripper); shift ;;
|
||||
--no-sensors) LAUNCH_ARGS+=(--no-sensors); shift ;;
|
||||
--check|--print-urdf|--dry-run)
|
||||
LAUNCH_ARGS+=("$1"); PASS_ONLY=1; shift ;;
|
||||
--world)
|
||||
[[ -n "${2:-}" ]] || { echo "--world 需要跟一个路径" >&2; exit 2; }
|
||||
LAUNCH_ARGS+=(--world "$2"); shift 2 ;;
|
||||
-h|--help) sed -n '2,18p' "$0"; exit 0 ;;
|
||||
*) echo "未知参数: $1(用 --help 看用法)" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -f "${ROOT}/install_sim/setup.bash" ]]; then
|
||||
echo "install_sim/ 还不存在。先跑:" >&2
|
||||
echo " bash scripts/build_sim.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# sim_env.sh 会先退到 base 再激活目标环境,避免同一 shell 里来回切造成的
|
||||
# PATH/PYTHONPATH 残留(详见 scripts/lib/conda_switch.sh 顶部的实测说明)。
|
||||
# shellcheck disable=SC1091
|
||||
source "${ROOT}/scripts/sim_env.sh"
|
||||
|
||||
if ! command -v ign >/dev/null 2>&1; then
|
||||
echo "找不到 ign(Gazebo Fortress 的 CLI)。先跑:" >&2
|
||||
echo " bash scripts/install_sim_env.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${PASS_ONLY}" -eq 1 ]]; then
|
||||
# 这些选项不启动任何东西,只准备描述,直接转发。
|
||||
exec ros2 run arm_sim_gazebo gazebo_sim_launcher "${LAUNCH_ARGS[@]}"
|
||||
fi
|
||||
|
||||
echo "=== 启动 Gazebo 仿真 ==="
|
||||
echo " 环境 : ${CONDA_DEFAULT_ENV:-?} ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-?}"
|
||||
echo " 退出方式 : Ctrl-C"
|
||||
echo
|
||||
echo " 首次启动约需 10~20 秒(Gazebo 初始化 + spawn + 拉控制器)。"
|
||||
echo " 验收: ros2 control list_controllers 应看到两个 active"
|
||||
echo
|
||||
|
||||
exec ros2 run arm_sim_gazebo gazebo_sim_launcher "${LAUNCH_ARGS[@]}"
|
||||
@@ -63,6 +63,47 @@ export ROS2_WS="${ROOT}"
|
||||
# 真机用的是默认 domain,两者天然隔离。
|
||||
export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-77}"
|
||||
|
||||
# =============================================================================
|
||||
# Gazebo 的插件与资源搜索路径 —— 必须在这里设,不能只在 launch 里设
|
||||
# =============================================================================
|
||||
# ros_gz_sim 的 gz_sim.launch.py 用的是 `from os import environ`,即在**生成
|
||||
# launch 描述时**读进程环境,然后把它算出来的值作为 additional_env 传给 gz。
|
||||
# 所以用 launch 的 SetEnvironmentVariable 补是没用的:那个值会被 gz_sim.launch.py
|
||||
# 自己算的那份字典覆盖掉。必须在 ros2 launch 之前就进到进程环境里。
|
||||
#
|
||||
# 1) 插件搜索路径
|
||||
# 缺了它,Gazebo 会在加载 gz_ros2_control 时报:
|
||||
# [Err] [SystemLoader.cc:94] Failed to load system plugin
|
||||
# [libgz_ros2_control-system.so] : couldn't find shared library.
|
||||
# 而库其实就在 $CONDA_PREFIX/lib 下(`ldd` 也全齐)。原因是 gz 的
|
||||
# SystemLoader 先在搜索路径里找文件,找不到就直接放弃,**不会**去
|
||||
# dlopen 一个裸文件名(所以 conda 二进制的 RPATH 兜不住)。
|
||||
# 又因为 gz_sim.launch.py 只把 LD_LIBRARY_PATH 拼进搜索路径,而 conda 激活
|
||||
# 并不往 LD_LIBRARY_PATH 里加 $CONDA_PREFIX/lib,于是这条路径永远缺失。
|
||||
#
|
||||
# 2) 资源搜索路径
|
||||
# gz 的 URDF 解析器会把 mesh 的 package:// 改写成 model://,
|
||||
# 而 model://<名> 是去"资源路径下的子目录"里找的。因此这里放的必须是
|
||||
# **各包 share 目录的上一层**(该层的子目录名就是包名),不是 share 目录本身。
|
||||
# 缺了它的表现是一堆非致命但很吵的报错(视觉网格加载不出来),
|
||||
# 并且**碰撞网格同样加载不出来** —— 后者会让装配任务的接触判据失效。
|
||||
if [[ -n "${CONDA_PREFIX:-}" ]]; then
|
||||
export GZ_SIM_SYSTEM_PLUGIN_PATH="${CONDA_PREFIX}/lib${GZ_SIM_SYSTEM_PLUGIN_PATH:+:${GZ_SIM_SYSTEM_PLUGIN_PATH}}"
|
||||
# IGN_* 是 Fortress(改名之前)用的名字,两个都设,兼容新旧插件查找逻辑
|
||||
export IGN_GAZEBO_SYSTEM_PLUGIN_PATH="${CONDA_PREFIX}/lib${IGN_GAZEBO_SYSTEM_PLUGIN_PATH:+:${IGN_GAZEBO_SYSTEM_PLUGIN_PATH}}"
|
||||
fi
|
||||
|
||||
_res_paths=()
|
||||
for _d in "${ROOT}"/install_sim/*/share; do
|
||||
[[ -d "${_d}" ]] && _res_paths+=("${_d}")
|
||||
done
|
||||
_res_paths+=("${ROOT}/devices/sim/gazebo/models") # 以后往这里放自定义模型
|
||||
_res_joined="$(IFS=:; echo "${_res_paths[*]}")"
|
||||
|
||||
export GZ_SIM_RESOURCE_PATH="${_res_joined}${GZ_SIM_RESOURCE_PATH:+:${GZ_SIM_RESOURCE_PATH}}"
|
||||
export IGN_GAZEBO_RESOURCE_PATH="${_res_joined}${IGN_GAZEBO_RESOURCE_PATH:+:${IGN_GAZEBO_RESOURCE_PATH}}"
|
||||
unset _res_paths _res_joined _d
|
||||
|
||||
# 仿真与真机共用同一份 site.yaml —— 这样"仿真里调好的参数搬到真机也生效"。
|
||||
# shellcheck disable=SC1091
|
||||
source "${ROOT}/scripts/lib/load_site_config.sh" 2>/dev/null || true
|
||||
|
||||
Reference in New Issue
Block a user