Files
ros_contorl/scripts/stop_sim_gz.sh
T
arm_dev 6922b18187 仿真夹爪适配器:服务接口对齐真机,开口按正运动学换算成真毫米
为什么
------
应用层只认 arm_msgs/srv/GripperExecute(开口毫米),真机与仿真必须同一套接口。
但 GN01 的 URDF 里 <mimic> 是**线性近似**:直接按线性系数下发 finger_width_joint,
"命令开 20 mm"在 Gazebo 里量出来是 21.75 mm —— 抓不住工件,而且看起来像算法问题。

实测(2026-09-24,Gazebo 内量两指尖间距):
  线性下发 20 mm → 实际 21.74 mm(偏差 1.74 mm,超 2 mm 抓取容差的前半步)
  走 FK 反解     → 实际 20.00 mm(≤0.5 mm)

怎么改
------
- 新增 sim_gripper_kin.py:解析 URDF 正运动学,算出"主关节值 ↔ 真实开口 mm"的
  单调曲线,反解用二分。曲线非单调/零行程时**主动报错**,不静默退化成线性。
- sim_gripper.py:开口换算全部走该曲线;容差统一改成毫米。
- sim_gripper_adapter.py:反馈改取**从动关节**(见下);is_gripped 按毫米判定。
- 新增 check_sim_gripper.py:三层验收(服务/开口一致/指尖真实几何行程 + 对称性)。

踩到的坑
--------
- finger_width_joint 挂在无质量的 unused_finger_width_link 上,与手指**没有机械
  连接**:命令到 0.100,它的 /joint_states 恒为 1.4e-08,6 个从动关节才准到
  0.7854。反馈若取它,/gripper/status 会永远报"开口 0 mm"。
- 残留进程会伪造出"代码写错了"的现场:旧 sim_gripper_adapter 与新实例抢话题,
  验收读到"指尖间距 0.03 mm"(真实 127 mm);孤儿 static_transform_publisher
  用 latched /tf_static 把旧相机外参按在新仿真上。
  → 新增 scripts/lib/sim_procs.sh;run_sim_gz.sh 用 setsid 独立进程组,
    PGID 记进 log_sim/sim.pgid,stop 时整组 kill;启动前预检拒绝带病启动。
- 验收脚本自己也错过两次(把 TF 的米当毫米比 → 127 mm 显示成 0.13 mm;
  用会转动指垫的中点漂移判对称 → 必然位移 16 mm)。判据改成"两侧各走多远",
  单位只在 _dist_mm 里换算一次。

验证
----
  python3 -m pytest src/arm_sim_gazebo/test/ -q          → 307 passed
  python3 scripts/check_sim_gripper.py                   → 三层全过
     第 2 层:请求/反馈开口最大偏差 < 0.5 mm
     第 3 层:期望行程 vs 实际行程、左右行程差,均 < 1.5 mm
  bash scripts/stop_sim_gz.sh --list                     → 整组列出(含 static tf)

文档
----
- 仿真手册:新增 §4.4(开口 mm 怎么换算)、坑 12(宽度关节不动、
  反馈必须取从动关节);停止一节改写成进程组语义。
- FRAMEWORK_RULES:新增 R4.11(残留进程会伪造现场)、R4.12(验收判据要挑
  与实现无关的物理量);R4.9 表补夹爪;清单补 13/14 两条。
- HANDBOOK:新增 §3.4「在仿真里开合夹爪」,补 §4.4 的两条新教训。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 09:46:09 +08:00

135 lines
5.7 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# =============================================================================
# scripts/stop_sim_gz.sh —— 干净地停掉 Gazebo 仿真栈
#
# 为什么要专门写一个脚本,而不是让你敲 pkill:
#
# ⚠️ `pkill -f <模式>` 在"用脚本启动仿真"的场景下会**把自己的启动壳一起杀掉**。
# 因为那条命令自己的命令行里就含那个模式字符串,pkill -f 匹配整条命令行,
# 于是它先匹配到自己所在的 shell。表现是:命令返回得飞快、什么输出都没有、
# 仿真也没停成 —— 而这看起来像脚本写错了,极难往"进程匹配"上想。
#
# 本脚本的做法:扫描候选 PID → 排除自己/祖先/扫描自身的临时进程 → kill,
# 最后**再扫一遍复查**(杀掉后仍有残留就报出来)。
# 所以它可以在任何调用链里安全使用(包括被其它脚本 `source`/调用)。
#
# 用法:
# bash scripts/stop_sim_gz.sh # 停仿真(TERM,等 5s,仍在则 KILL)
# bash scripts/stop_sim_gz.sh --list # 只列出会被停掉的进程,不动手
# =============================================================================
set -uo pipefail
LIST_ONLY=0
[ "${1:-}" = "--list" ] && LIST_ONLY=1
# \"哪些进程算仿真栈\"这张表**不在本文件里**,在 scripts/lib/sim_procs.sh:
# 它同时被 run_sim_gz.sh(启动前的预检)使用,两处共用一份实现,不会漂移。
#
# 历史教训(都真实发生过,所以规则才写这么细):
# - 漏过 robot_state_publisher:脚本说"已停止",它还在跑;下一次启动时
# 继续发上一份 /robot_description 与 TF,表现成"改了描述却不生效"。
# - 漏过 static_transform_publisher:更阴 —— 它发的是 latched 的 /tf_static,
# 残留进程会一直把**旧相机外参**按在新仿真的 TF 树上。
# - 漏过 sim_gripper_adapter:残留的适配器与新适配器**同时**订阅 /joint_states、
# 同时发布 /gripper/status,服务调用还会被随机分给其中一个。
# 验收脚本读到的开口恒为 0 mm、指尖间距 0.03 mm(实际 127 mm),
# 看起来就像夹爪代码算错了 —— 这是最难查的一类残留。
#
# 所以停止分两步:
# ① 主通道:run_sim_gz.sh 用 setsid 起仿真,PGID 记在 log_sim/sim.pgid,
# `kill -TERM -PGID` 一次带走整组(**不依赖任何模式串**);
# ② 兜底:模式扫描(点名表 + install_sim/ 兜底),收拾历史孤儿与
# ign 自己 fork 出去的进程;杀完再扫一遍,有残留就报出来并非零退出。
# shellcheck source=lib/sim_procs.sh
source "$(dirname "${BASH_SOURCE[0]}")/lib/sim_procs.sh"
PGID="$(sim_pgid_read)"
if [ -n "$PGID" ] && sim_pgid_alive "$PGID"; then
if ! sim_pgid_leader_is_ours "$PGID"; then
echo "⚠ log_sim/sim.pgid 里的进程组 $PGID 看起来不是仿真栈(PID 可能被复用),忽略它。"
PGID=""
fi
else
PGID=""
fi
if [ "$LIST_ONLY" = "1" ]; then
if [ -n "$PGID" ]; then
echo "仿真的进程组:$PGID(kill -TERM -$PGID 可整组停掉)"
ps -o pid=,args= -g "$PGID" 2>/dev/null | cut -c1-150
fi
fi
# 模式扫描的候选(--list 时先打印,再决定动不动手)。
mapfile -t targets < <(sim_proc_targets)
if [ "${#targets[@]}" = "0" ] && [ -z "$PGID" ]; then
echo "没有正在运行的仿真进程(已排除本脚本自身与调用它的 shell)。"
exit 0
fi
if [ -n "$PGID" ] && [ "$LIST_ONLY" != "1" ]; then
echo "停止仿真进程组 $PGID(整组,含不在 install_sim/ 下的节点)…"
sim_pgid_stop "$PGID"
# 组信号发完后重新扫一遍:可能还有孤儿。
mapfile -t targets < <(sim_proc_targets)
fi
if [ "${#targets[@]}" = "0" ]; then
echo "仿真已停止。"
exit 0
fi
echo "将停止以下进程:"
for pid in "${targets[@]}"; do
printf ' %-7s %s\n' "$pid" "$(ps -o args= -p "$pid" 2>/dev/null | cut -c1-140)"
done
if [ "$LIST_ONLY" = "1" ]; then
echo "(--list:未动手)"
exit 0
fi
for pid in "${targets[@]}"; do
kill -TERM "$pid" 2>/dev/null
done
# 给 HAL 一点时间退出。Gazebo 的渲染线程与 DART 的求解线程不会立刻收尾,
# 立刻 KILL 有可能留下共享内存段,下一次启动表现成"渲染初始化失败"。
for _ in $(seq 1 10); do
alive=0
for pid in "${targets[@]}"; do
kill -0 "$pid" 2>/dev/null && alive=1
done
[ "$alive" = "0" ] && break
sleep 0.5
done
for pid in "${targets[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
echo " PID $pid 没响应 TERM,用 KILL"
kill -KILL "$pid" 2>/dev/null
fi
done
# ---------------------------------------------------------------------------
# 复查:杀完再扫一遍。
# 这一步是为了让"表漏了某个进程"变成**看得见的告警**,而不是
# "脚本说已停止、实际有残留"。robot_state_publisher 与 sim_gripper_adapter
# 都这么漏过,而后者导致的假象是"夹爪算错了"。
# ---------------------------------------------------------------------------
sleep 0.5
mapfile -t leftover < <(sim_proc_targets)
if [ "${#leftover[@]}" != "0" ]; then
echo "⚠ 仍有仿真相关进程存活(说明它既不在 lib/sim_procs.sh 的点名表里,"
echo " 也不在 install_sim/ 下 —— 请把它加进点名表,或让它从 install_sim/ 启动):"
for pid in "${leftover[@]}"; do
printf ' %-7s %s\n' "$pid" "$(ps -o args= -p "$pid" 2>/dev/null | cut -c1-140)"
done
echo "仿真已停止,但有残留 —— 残留会与下一次启动抢话题/TF。"
exit 3
fi
echo "仿真已停止。"