6baf989595
实测(2026-09-28)在仿真里上料失败,一路查到 IK / TF / 时钟上,全是被冤枉的。 根因是两个真机侧一直缺的东西: 1) **真机侧没有停止脚本**。仿真侧早有 stop_sim_gz.sh(R4.10/R4.11:残留会伪造出 "代码写错了"的现场),真机侧只有 run_real.sh 里两行 pkill。而真机栈里有一族 **独立于 ros2_control_node 存活**的进程 —— 上一轮栈的主体早没了,只剩 real_trajectory_adapter / named_pose_server / arm_status_node / joint_state_publisher 四条,两条 pkill 一条都匹配不到。 危害不是占资源而是**冒名顶替**:残留的 adapter 照样订阅 /trajectory、 残留的 joint_state_publisher 照样发 /joint_states(mock 的静态关节角)。 现象极具误导性:控制器 feedback 与 reference 完全一致(控制器没坏), TF 却显示末端一动不动。 新增 stop_real.sh + lib/real_procs.sh:扫描 = 点名表 + `install/` 前缀兜底, 按 ROS_DOMAIN_ID 判别真机/仿真(两边有同名的 robot_state_publisher, 否则停真机会把正在跑的仿真杀一半),杀完复查残留并非零退出。 run_real.sh 启动前的清理改用它,不再各写一份会漂移的模式表。 真机侧**有意不**用仿真的"进程组"法:真机栈是前台交互跑的,没有独立进程组, 它的 PGID 就是调用它的 shell —— 记下来再 kill 会杀掉自己的终端。 2) **真机侧不强制 ROS_DOMAIN_ID**。sim_env.sh 早就强制 77 并写明"隔离不能靠 『环境里恰好没设』",真机侧没有对应保护:先 source 过 sim_env.sh 的 shell 里 再起真机栈,真机栈就落到**仿真的域**上,节点名撞车、/joint_states 与 /tf 各有两个来源互相盖。补 lib/ros_domain.sh(真机 0 / 覆盖用 REAL_ROS_DOMAIN_ID), env.sh、run_real.sh、run_sim.sh 三处共用一份实现 —— 成对强制,否则一侧的 强制只是把运气挪到了另一侧。 run_sim.sh 特别容易漏:它名为 sim,用的却是**真机的 install/**、节点名与真机 完全一样,而且当时不管域;这次也钉到真机域 0。run_sim_gz.sh 文件头那句 "两者互不影响"是错的,一并改掉。 验证:stop_real.sh --list 在有仿真在跑时为空(REAL_PROC_INCLUDE_SIM=1 能看到 并按"域 77"标出);伪装一个 install/arm_control/lib/arm_control/arm_status_node 的孤儿进程能被点名。新增 6 项单测钉住域的四种输入 + 一个不许误报。 Co-authored-by: Cursor <cursoragent@cursor.com>
148 lines
6.9 KiB
Bash
Executable File
148 lines
6.9 KiB
Bash
Executable File
#!/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:真物理、真接触
|
||
# 两者都不需要真机。要验证力控/接触类逻辑只能用后者。
|
||
#
|
||
# ⚠️ 它们**不是**"互不影响"的 —— 早先这句注释是错的,2026-09-28 踩过一次:
|
||
# run_sim.sh 用的是**真机的 install/**、节点名与真机完全一样,而且它当时
|
||
# **不管 ROS_DOMAIN_ID**,调用者环境里带 77(跑过仿真)就落到这个仿真的域上。
|
||
# 于是 mock 栈与 Gazebo 抢 /joint_states:控制器 feedback 与 reference
|
||
# 完全一致(控制器没坏),TF 却显示末端一动不动,表现为"仿真上料失败"。
|
||
# 现在两侧都钉死了域:run_sim.sh 走真机域 0(见 lib/ros_domain.sh),
|
||
# 本脚本走 77(见 sim_env.sh),所以可以并存而不再互相冒名顶替。
|
||
#
|
||
# ⚠️ 这个脚本此前**不存在**,却被 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
|
||
# 这些选项不启动任何东西,只准备描述,直接转发。
|
||
# (所以放在"残留进程"预检**之前**:手上有仿真在跑时,照样应该能 --print-urdf。)
|
||
exec ros2 run arm_sim_gazebo gazebo_sim_launcher "${LAUNCH_ARGS[@]}"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 预检:不许在"上一轮仿真还有残留"的情况下启动
|
||
# ---------------------------------------------------------------------------
|
||
# ⚠️ 这不是洁癖。实测过的最坏情况:残留了一个 sim_gripper_adapter,新起一个之后
|
||
# 两个适配器**同时**订阅 /joint_states、同时发布 /gripper/status,
|
||
# 服务调用还会被随机分给其中一个 —— 验收脚本读到的开口恒为 0 mm、
|
||
# 指尖间距 0.03 mm(真实值 127 mm),看起来完全像夹爪换算写错了。
|
||
# 残留的 static_transform_publisher 更阴:它发 latched 的 /tf_static,
|
||
# 会一直把**旧外参**按在新仿真的 TF 树上。
|
||
# 所以这里宁可拒绝启动,也不给你一个"看起来能跑、数据是错的"仿真。
|
||
# shellcheck source=lib/sim_procs.sh
|
||
source "${ROOT}/scripts/lib/sim_procs.sh"
|
||
|
||
mapfile -t running < <(sim_proc_targets)
|
||
if [[ "${#running[@]}" != "0" ]]; then
|
||
{
|
||
echo "已有仿真相关进程在跑,拒绝启动第二个:"
|
||
for pid in "${running[@]}"; do
|
||
printf ' %-7s %s\n' "$pid" "$(ps -o args= -p "$pid" 2>/dev/null | cut -c1-120)"
|
||
done
|
||
echo
|
||
echo "先停掉它们: bash scripts/stop_sim_gz.sh"
|
||
echo "只想看看残留是谁、不动手:bash scripts/stop_sim_gz.sh --list"
|
||
} >&2
|
||
exit 3
|
||
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
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 起在自己的**会话/进程组**里,并把 PGID 记下来
|
||
# ---------------------------------------------------------------------------
|
||
# 这样 stop_sim_gz.sh 可以用 `kill -TERM -PGID` 一次性带走整栈(含
|
||
# robot_state_publisher / static_transform_publisher 这些"不在 install_sim 下"
|
||
# 的节点),不必依赖模式匹配 —— 模式表漏一个就留一个孤儿的教训见脚本注释。
|
||
#
|
||
# 代价:进程组不在前台,终端里的 Ctrl-C **不会**传到仿真。所以这里自己接
|
||
# SIGINT/SIGTERM,转发成组信号,行为与以前"Ctrl-C 停仿真"一致。
|
||
PGID_FILE="$(sim_pgid_file)"
|
||
mkdir -p "$(dirname "${PGID_FILE}")"
|
||
|
||
setsid bash -c 'echo $$ > "$1"; shift; exec "$@"' _ "${PGID_FILE}" \
|
||
ros2 run arm_sim_gazebo gazebo_sim_launcher "${LAUNCH_ARGS[@]}" &
|
||
SIM_PID=$!
|
||
|
||
_forward_signal() {
|
||
local pgid
|
||
pgid="$(sim_pgid_read)"
|
||
if [[ -n "${pgid}" ]]; then
|
||
echo
|
||
echo "收到中断信号,正在停止整栈(进程组 ${pgid})…" >&2
|
||
sim_pgid_stop "${pgid}"
|
||
fi
|
||
}
|
||
trap _forward_signal INT TERM
|
||
|
||
wait "${SIM_PID}"
|
||
status=$?
|
||
rm -f "${PGID_FILE}"
|
||
exit "${status}"
|