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}"