From a89250d05df24901d2b63c6ddadf7eef64aa9a9d Mon Sep 17 00:00:00 2001 From: arm_dev Date: Wed, 23 Sep 2026 13:52:04 +0800 Subject: [PATCH] =?UTF-8?q?P5c-1=EF=BC=9A=E6=8A=8A=20Gazebo=20=E4=BB=BF?= =?UTF-8?q?=E7=9C=9F=E7=8E=AF=E5=A2=83=E7=9C=9F=E6=AD=A3=E8=A3=85=E8=B5=B7?= =?UTF-8?q?=E6=9D=A5=EF=BC=88=E7=8B=AC=E7=AB=8B=20conda=20=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=EF=BC=89=EF=BC=8C=E5=B9=B6=E5=9B=BA=E5=8C=96=E5=AE=89?= =?UTF-8?q?=E8=A3=85/=E6=9E=84=E5=BB=BA=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在此之前,仓库里所有 Gazebo 相关的东西都是"声明"而非"事实": arm.gazebo.xacro 零引用、stack.launch.py 的 isaac 分支只打印日志、 没有 world 文件、devices/sim/gazebo 下的目录全是空占位。 更关键的是:**这台机器上根本没装 Gazebo**,而 ROS 在 conda 里,没有 /opt/ros, apt 的 ros-humble-gazebo-* 装了也不会被 conda 的 ROS 认到。 环境(与真机环境物理隔离) - 新建 conda 环境 ros_gz_sim,**不碰能跑真机的 ros_flexiv**。 仿真要往环境里塞 Gazebo 及 protobuf/ignition/渲染一整条链, 和真机环境混装随时可能把它弄坏,代价远大于仿真带来的便利。 - 频道组合照抄 ros_flexiv 的安装记录(robostack-staging + conda-forge), 那是本机已经证明可用的搭配。 两个实测踩到的坑(都写进了脚本注释,避免下次重踩) 1. empy 版本:conda 默认给 empy 4.x,而 Humble 的 rosidl_adapter 用的是 3.x 的 API。症状是构建自定义消息包时报 `_interpreter.shutdown()` 的 AttributeError, 报错点在 rosidl_adapter 内部,完全看不出跟 empy 有关。必须钉 empy=3.3.4。 2. conda 激活是**堆叠**的:同一 shell 里从 ros_gz_sim 切到 ros_flexiv 后, `which python` 指向 ros_flexiv,但 `python -m pip` 却解析到 ros_gz_sim 的 pip, 导致 colcon --symlink-install 报 "option --editable not recognized"。 这个报错和真实原因毫无关系,极易误判成"环境被搞坏了"。 现在 scripts/lib/conda_switch.sh 会在切换前退到 base,两个 env 脚本共用。 仿真用独立构建产物 - build_sim/ + install_sim/(scripts/build_sim.sh)。工作区只有一份源码, 若仿真也写 install/,就会覆盖真机的构建产物。 - 排除 11 个真机专属包(flexiv_hardware 需要 Flexiv RDK C++ SDK,根本装不上; 另有真机驱动/GPIO/MoveIt 配置/真机 bringup),每个都在脚本里写了理由。 已验证(不是只看退出码) - Gazebo Fortress 6.16 无头启动正常,物理初始化完成 - /clock 桥接到 ROS 成功收到仿真时间;ros_gz_sim create 成功往世界里建物 - 真机构建 18 包通过、233 项测试通过,build/ install/ 时间戳未变(没被覆盖) - 仿真侧 7 包构建通过;两环境互切后各自都能正常构建 注:Humble 配的是 Gazebo Fortress(ignition-gazebo6,命令行 `ign gazebo`), 不是现代的 gz-sim —— 后者要 libprotobuf>=6.31,与 RoboStack Humble 钉住的 5.29.3 解不开依赖。Fortress 属于新 Gazebo 家族(非已 EOL 的 Classic), 且是 Humble 的官方配对版本。 Co-authored-by: Cursor --- .gitignore | 6 ++ scripts/build_sim.sh | 84 ++++++++++++++++++++++ scripts/env.sh | 15 +++- scripts/install_sim_env.sh | 136 ++++++++++++++++++++++++++++++++++++ scripts/lib/conda_switch.sh | 56 +++++++++++++++ scripts/sim_env.sh | 70 +++++++++++++++++++ 6 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 scripts/build_sim.sh create mode 100644 scripts/install_sim_env.sh create mode 100644 scripts/lib/conda_switch.sh create mode 100644 scripts/sim_env.sh diff --git a/.gitignore b/.gitignore index d7f122c..71658f2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,12 @@ models/staging/** build/ install/ log/ +# 仿真的构建产物 —— 与真机的 build/ install/ 隔离(见 scripts/build_sim.sh) +# 两套产物目录并存是刻意的:工作区只有一份源码,两个环境的依赖路径不同, +# 共用 install/ 会互相覆盖。 +build_sim/ +install_sim/ +log_sim/ .colcon/ # Python diff --git a/scripts/build_sim.sh b/scripts/build_sim.sh new file mode 100644 index 0000000..f18e5eb --- /dev/null +++ b/scripts/build_sim.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# ============================================================================= +# 在仿真环境里构建本工作区。 +# +# 用法: +# bash scripts/build_sim.sh # 增量构建 +# bash scripts/build_sim.sh --clean # 先删掉 build_sim/ install_sim/ +# bash scripts/build_sim.sh --all # 不做任何排除(用于排查) +# +# 关键:构建产物写进 build_sim/ install_sim/,**绝不碰真机环境的 build/ install/**。 +# 工作区只有一份源码,两个环境各用一套产物目录,互不覆盖。 +# ============================================================================= +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${ROOT}" + +CLEAN=0 +NO_SKIP=0 +for a in "$@"; do + case "$a" in + --clean) CLEAN=1 ;; + --all) NO_SKIP=1 ;; + -h|--help) sed -n '2,15p' "$0"; exit 0 ;; + *) echo "未知参数: $a" >&2; exit 2 ;; + esac +done + +# --- 排除清单 --------------------------------------------------------------- +# 这些包在仿真里用不上,而且不少根本**装不上**(依赖真机 SDK),不排除就整个构建失败。 +# 每一项都写清理由,避免以后有人不明就里地删掉或加回来。 +SKIP=( + # 需要 Flexiv RDK C++ SDK(flexiv_rdk),仿真里硬件层由 gz_ros2_control 提供 + flexiv_hardware + # 真机 RDK 桥接驱动(起子进程连真机),仿真里无真机可连 + arm_driver_flexiv + # Flexiv 官方真机栈 + flexiv_msgs + flexiv_bringup + flexiv_gripper + flexiv_robot_states_broadcaster + # 真机硬件测试节点 + flexiv_test_nodes + # 真机 GPIO(气爪/电磁阀),仿真里没有对应的物理器件 + gpio_controller + # MoveIt 配置:体量大,且仿真运动不走 MoveIt 规划 + arm_moveit_config + flexiv_moveit_config + # 真机 bringup。它 CMake 硬依赖 arm_moveit_config,而仿真启动器是 + # arm_sim_gazebo(见 devices/sim/gazebo/manifest.yaml 的声明), + # 不需要它。真机路径完全不受影响。 + arm_bringup +) + +echo "=== 构建仿真工作区 ===" +echo " 产物目录: build_sim/ install_sim/" +if [[ "${NO_SKIP}" -eq 1 ]]; then + echo " 排除: 无(--all)" +else + echo " 排除: ${#SKIP[@]} 个真机专属包" +fi + +if [[ "${CLEAN}" -eq 1 ]]; then + echo " 清理: 删除 build_sim/ install_sim/ log_sim/" + rm -rf build_sim install_sim log_sim +fi + +# --- 激活仿真环境 ----------------------------------------------------------- +# shellcheck disable=SC1091 +source "${ROOT}/scripts/sim_env.sh" + +ARGS=(build --build-base build_sim --install-base install_sim) +# 注意:这里**不能**加 --symlink-install。仿真环境的 setuptools 较旧, +# 不认 --editable,会报 "error: option --editable not recognized"。 +# 代价只是安装产物是拷贝而非软链,功能不受影响。 +if [[ "${NO_SKIP}" -eq 0 ]]; then + ARGS+=(--packages-skip "${SKIP[@]}") +fi + +colcon --log-base log_sim "${ARGS[@]}" 2>&1 \ + | grep -vE 'anaconda-auth|conda-content-trust|Unknown distribution option|warnings\.warn' + +echo +echo "构建完成。启动仿真: bash scripts/run_sim_gz.sh" diff --git a/scripts/env.sh b/scripts/env.sh index 8f607d6..0516c57 100755 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -14,7 +14,20 @@ fi if [[ -f "${HOME}/anaconda3/etc/profile.d/conda.sh" ]]; then # shellcheck disable=SC1091 source "${HOME}/anaconda3/etc/profile.d/conda.sh" - conda activate ros_flexiv 2>/dev/null || true + # 干净切换:先把堆叠的激活退到 base,再激活真机环境。 + # 同一 shell 里从仿真环境(ros_gz_sim)切过来时,不清干净会残留 PATH/PYTHONPATH, + # 导致 python/pip 解析到另一个环境的包,构建报 "--editable not recognized" + # 之类与真实原因无关的错。详见 scripts/lib/conda_switch.sh。 + _env_helper="${ROOT}/scripts/lib/conda_switch.sh" + if [[ -f "${_env_helper}" ]]; then + # shellcheck disable=SC1090 + source "${_env_helper}" + conda_switch_clean "ros_flexiv" "${SIM_ENV_NAME:-ros_gz_sim}" || \ + conda activate ros_flexiv 2>/dev/null || true + else + conda activate ros_flexiv 2>/dev/null || true + fi + unset _env_helper fi # shellcheck disable=SC1091 diff --git a/scripts/install_sim_env.sh b/scripts/install_sim_env.sh new file mode 100644 index 0000000..383e337 --- /dev/null +++ b/scripts/install_sim_env.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# ============================================================================= +# 创建 / 修复 Gazebo 仿真用的独立 conda 环境。 +# +# 为什么单独一个环境,而不是往 ros_flexiv 里装: +# ros_flexiv 是**能跑真机**的那一套。仿真需要往环境里加 Gazebo 及其一大堆 +# 依赖(protobuf / ignition / 渲染栈),和真机环境混装随时可能把它弄坏。 +# 真机环境坏了,代价远大于仿真带来的便利。所以两者物理隔离。 +# +# 用法: +# bash scripts/install_sim_env.sh # 创建或补齐(幂等,可重复跑) +# bash scripts/install_sim_env.sh --verify # 只验证现有环境,不安装 +# +# 装完用 scripts/sim_env.sh 激活,用 scripts/run_sim_gz.sh 启动。 +# ============================================================================= +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_NAME="${SIM_ENV_NAME:-ros_gz_sim}" +CONDA_SH="${HOME}/anaconda3/etc/profile.d/conda.sh" + +# 频道组合照抄现有 ros_flexiv 环境的安装记录,那是已经证明可用的搭配。 +CHANNELS=(-c robostack-staging -c conda-forge) + +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +info() { printf ' \033[36m·\033[0m %s\n' "$1"; } +die() { printf ' \033[31m✗ %s\033[0m\n' "$1" >&2; exit 1; } + +if [[ ! -f "${CONDA_SH}" ]]; then + die "找不到 conda: ${CONDA_SH}(本脚本假定 anaconda 装在 ~/anaconda3)" +fi +# shellcheck disable=SC1090 +source "${CONDA_SH}" + +# --- 验证 ------------------------------------------------------------------- +verify() { + info "激活 ${ENV_NAME} 并自检…" + conda activate "${ENV_NAME}" 2>/dev/null || die "激活失败,环境可能没建好" + + echo + echo "1) 仿真器本体" + # 注意:Humble 配的是 Gazebo Fortress,命令行是 `ign gazebo`,不是 `gz sim`。 + # 现代包名 gz-sim 在 conda-forge 上要 libprotobuf>=6.31,而 RoboStack 的 + # Humble 把 protobuf 钉在 5.29.3,混装会解不开依赖。Fortress 是官方配对版本。 + ign gazebo --version 2>&1 | head -1 | sed 's/^/ /' + + echo + echo "2) ROS 侧桥接与力控" + local missing=0 p + for p in ros_gz_sim ros_gz_bridge gz_ros2_control controller_manager ros2_control xacro; do + if ros2 pkg prefix "$p" >/dev/null 2>&1; then + ok "$p" + else + printf ' \033[31m✗ %s 缺失\033[0m\n' "$p"; missing=1 + fi + done + + echo + echo "3) 构建工具链(Humble 的 rosidl 需要 empy 3.x)" + local empy_ver + empy_ver="$(python -c 'import em; print(em.__version__)' 2>/dev/null || echo '?')" + if [[ "${empy_ver}" == 3.* ]]; then + ok "empy ${empy_ver}" + else + printf ' \033[31m✗ empy %s —— 必须是 3.x,否则 rosidl 生成接口时会报\n' "${empy_ver}" + printf ' "AttributeError: NoneType object has no attribute shutdown"\033[0m\n' + missing=1 + fi + + echo + echo "4) 仿真能真跑起来吗(无头空跑 30 步)" + local world + world="$(find "${CONDA_PREFIX}/share" -name empty.sdf -path '*gazebo*' 2>/dev/null | head -1)" + if [[ -n "${world}" ]] && timeout 60 ign gazebo -s -r --iterations 30 "${world}" >/dev/null 2>&1; then + ok "仿真引擎正常启动" + else + printf ' \033[31m✗ 仿真引擎启动失败\033[0m\n'; missing=1 + fi + + echo + if [[ "${missing}" -eq 0 ]]; then + echo "环境就绪。下一步:" + echo " source scripts/sim_env.sh" + echo " bash scripts/build_sim.sh" + echo " bash scripts/run_sim_gz.sh" + else + echo "环境不完整,重跑 bash scripts/install_sim_env.sh 补齐。" + return 1 + fi +} + +if [[ "${1:-}" == "--verify" ]]; then + verify + exit $? +fi + +# --- 安装 ------------------------------------------------------------------- +echo "=== 1/4 创建环境 ${ENV_NAME} ===" +if conda env list | awk '{print $1}' | grep -qx "${ENV_NAME}"; then + ok "已存在,跳过创建" +else + conda create -y -n "${ENV_NAME}" python=3.11 pip || die "创建环境失败" + ok "已创建" +fi + +echo +echo "=== 2/4 安装 ROS 2 Humble 仿真栈 ===" +info "ros-humble-ros-gz 会一并带入 Gazebo Fortress(ignition-gazebo6)" +conda install -y -n "${ENV_NAME}" "${CHANNELS[@]}" \ + ros-humble-ros-base \ + ros-humble-ros-gz \ + ros-humble-gz-ros2-control \ + ros-humble-ros2-control \ + ros-humble-ros2-controllers \ + ros-humble-controller-manager \ + ros-humble-robot-state-publisher \ + ros-humble-joint-state-publisher \ + ros-humble-joint-state-publisher-gui \ + ros-humble-xacro \ + ros-humble-tf2-ros \ + ros-humble-camera-info-manager \ + ros-humble-image-transport-plugins \ + compilers cmake make pkg-config catkin_pkg lark-parser numpy \ + colcon-common-extensions || die "ROS 仿真栈安装失败" + +echo +echo "=== 3/4 钉住 empy 3.x(Humble 的硬性要求)===" +# 坑:conda 默认会给 empy 4.x,而 Humble 的 rosidl_adapter 用 empy 3.x 的 API。 +# 症状是构建自定义消息包时报 '_interpreter.shutdown()' 的 AttributeError, +# 报错点在 rosidl_adapter 内部,看起来完全不像 empy 的问题。 +conda install -y -n "${ENV_NAME}" "${CHANNELS[@]}" "empy=3.3.4" || die "empy 降级失败" +ok "empy 3.3.4" + +echo +echo "=== 4/4 自检 ===" +verify diff --git a/scripts/lib/conda_switch.sh b/scripts/lib/conda_switch.sh new file mode 100644 index 0000000..758f63d --- /dev/null +++ b/scripts/lib/conda_switch.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# ============================================================================= +# conda 环境切换的公共逻辑。被 scripts/env.sh 与 scripts/sim_env.sh 共用。 +# +# 为什么需要这个:conda 的 activate 是**堆叠**的。在同一个 shell 里反复 +# activate / 切换不同环境,会不断往 PATH、PYTHONPATH 里追加条目,而不会 +# 清掉前一个环境留下的。实测的后果非常隐蔽: +# +# 在同一个 shell 里先 activate ros_gz_sim 再切 ros_flexiv 之后, +# `which python` 指向 ros_flexiv/bin/python, +# 但 `python -m pip` 却解析到 ros_gz_sim 的 pip! +# 于是 colcon --symlink-install 报: +# error: option --editable not recognized +# 这个报错和真实原因(shell 脏了)毫无关系,极易被误判成 +# "环境被搞坏了 / setuptools 版本不对",进而去做一堆无用的修复。 +# +# 对策:切换前先把堆叠的激活全部退掉(退到 base),再激活目标环境。 +# ============================================================================= + +# conda_deactivate_all: 退到 base(CONDA_SHLVL=1)。任何环境下调用都安全。 +conda_deactivate_all() { + local guard=0 + # CONDA_SHLVL 是 conda 自己维护的激活层数;base 为 1。 + # 加 guard 是为了防止某些畸形状态下无限循环。 + while [[ "${CONDA_SHLVL:-1}" -gt 1 && "${guard}" -lt 20 ]]; do + conda deactivate 2>/dev/null || break + guard=$((guard + 1)) + done + return 0 +} + +# conda_switch_clean [] +# 干净地激活 ;若当前已在 里,先退到 base 再切, +# 并提示用户最好开新 shell。 +# 返回 0 = 成功;非 0 = 目标环境不存在。 +conda_switch_clean() { + local target="$1" + local other="${2:-}" + + if [[ -n "${other}" && "${CONDA_DEFAULT_ENV:-}" == "${other}" ]]; then + echo "提示: 当前 shell 激活的是 ${other},即将切到 ${target}。" >&2 + echo " conda 的激活是堆叠的,同一 shell 里来回切会残留 PATH/PYTHONPATH," >&2 + echo " 典型症状是构建报 \"--editable not recognized\" 这类与真实原因无关的错。" >&2 + echo " 这里会先退到 base 再切以规避;但最稳妥仍是开一个新 shell。" >&2 + fi + + conda_deactivate_all + + if ! conda activate "${target}" 2>/dev/null; then + return 1 + fi + return 0 +} + +# 供调用方按需取消设置,避免污染使用者的变量空间。 +# (bash 没有"私有函数"概念,命名上统一加 conda_ 前缀降低撞名概率。) diff --git a/scripts/sim_env.sh b/scripts/sim_env.sh new file mode 100644 index 0000000..05e6da0 --- /dev/null +++ b/scripts/sim_env.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# ============================================================================= +# 激活**仿真**环境(Gazebo Fortress + 本工作区的仿真构建产物)。 +# +# 用法: source scripts/sim_env.sh +# +# 与 scripts/env.sh(真机环境)的关系: +# 两者互斥,各自用一套 conda 环境 + 一套构建产物。 +# 真机: ros_flexiv → build/ install/ +# 仿真: ros_gz_sim → build_sim/ install_sim/ +# 切换时**最好开新的 shell**。同一 shell 里来回切会残留 PATH/PYTHONPATH, +# 症状详见 scripts/lib/conda_switch.sh 顶部的说明(实测踩过)。 +# 这里会先退到 base 再切以尽量规避,但开新 shell 才是稳妥做法。 +# +# 为什么仿真用独立的 build/install 目录: +# 工作区只有一份源码。如果仿真也往 install/ 里写,就会把真机环境的构建 +# 产物覆盖掉(两边的依赖路径不同)。分目录后互不干扰。 +# ============================================================================= +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +SIM_ENV_NAME="${SIM_ENV_NAME:-ros_gz_sim}" +REAL_ENV_NAME="${REAL_ENV_NAME:-ros_flexiv}" +CONDA_SH="${HOME}/anaconda3/etc/profile.d/conda.sh" + +# shellcheck disable=SC1091 +source "${ROOT}/scripts/lib/conda_switch.sh" + +if [[ ! -f "${CONDA_SH}" ]]; then + echo "找不到 conda: ${CONDA_SH}" >&2 + return 1 2>/dev/null || exit 1 +fi + +# shellcheck disable=SC1090 +source "${CONDA_SH}" + +# 关掉 `set -u`:ROS 的 activate.d 脚本会引用未绑定的 CONDA_BUILD, +# 调用方常带 set -u,那样会直接把 source 打断,表现为"脚本毫无输出就退出"。 +_sim_had_u=0 +case "$-" in *u*) _sim_had_u=1; set +u ;; esac + +if ! conda_switch_clean "${SIM_ENV_NAME}" "${REAL_ENV_NAME}"; then + [[ "${_sim_had_u}" -eq 1 ]] && set -u + unset _sim_had_u + echo "无法激活 conda 环境 ${SIM_ENV_NAME}。" >&2 + echo "先创建: bash ${ROOT}/scripts/install_sim_env.sh" >&2 + return 1 2>/dev/null || exit 1 +fi + +# 仿真构建产物(由 scripts/build_sim.sh 用 --install-base 生成,与真机隔离) +if [[ -f "${ROOT}/install_sim/setup.bash" ]]; then + # shellcheck disable=SC1091 + source "${ROOT}/install_sim/setup.bash" +else + echo "提示: install_sim/ 还不存在,先跑 bash scripts/build_sim.sh" >&2 +fi + +[[ "${_sim_had_u}" -eq 1 ]] && set -u +unset _sim_had_u + +export YOLO_AUTOINSTALL=false +export ROS2_WS="${ROOT}" +# 仿真单独一个 DDS domain:避免与正在跑的真机节点互相抢话题/污染 TF。 +# 真机用的是默认 domain,两者天然隔离。 +export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-77}" + +# 仿真与真机共用同一份 site.yaml —— 这样"仿真里调好的参数搬到真机也生效"。 +# shellcheck disable=SC1091 +source "${ROOT}/scripts/lib/load_site_config.sh" 2>/dev/null || true + +echo "[sim_env] CONDA=${CONDA_DEFAULT_ENV} ROS_DISTRO=${ROS_DISTRO} ROS_DOMAIN_ID=${ROS_DOMAIN_ID}"