真机侧补 stop_real.sh 与域隔离:残留的 mock 栈会把仿真/真机的 TF 盖成"静止"
实测(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>
This commit is contained in:
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# scripts/lib/real_procs.sh —— 真机进程扫描(被 stop_real.sh / run_real.sh 共用)
|
||||
#
|
||||
# 为什么要有这个文件
|
||||
# ----------------
|
||||
# 仿真侧早就有 lib/sim_procs.sh(点名表 + install_sim/ 兜底 + 杀完复查),
|
||||
# 真机侧**一直没有**对应物:只有 run_real.sh 里两行 pkill,
|
||||
# pkill -f '/controller_manager/ros2_control_node'
|
||||
# pkill -f 'arm_bringup.*stack.launch'
|
||||
# 而真机栈里还有一族**独立于这两条存活**的进程。2026-09-28 实测抓到的一次:
|
||||
# 上一轮栈的主体早就没了,只剩
|
||||
# real_trajectory_adapter / named_pose_server / arm_status_node /
|
||||
# joint_state_publisher
|
||||
# 四条(全在 install/arm_control/lib/arm_control/ 下),两条 pkill 一条都匹配不到。
|
||||
#
|
||||
# 危害不是"占内存",而是**冒名顶替**:
|
||||
# · 残留的 adapter 照样订阅 /trajectory、
|
||||
# · 残留的 named_pose_server 照样提供 /set_named_pose、
|
||||
# · 残留的 joint_state_publisher 照样发 /joint_states(mock 值的**静态**关节角),
|
||||
# 于是新一轮的请求被 DDS 负载均衡到"残留的那个",/joint_states 与 /tf 有两份
|
||||
# 来源互相盖。现象极具误导性:控制器 feedback 与 reference 完全一致(控制器没坏),
|
||||
# 但 TF 显示末端**一动不动** —— 顺着现象查会一路查到 IK / TF / 时钟上。
|
||||
#
|
||||
# 所以这里做三件事:
|
||||
# 1. run_real.sh 与 stop_real.sh 共用同一份扫描实现(不再各写一份会漂移的表);
|
||||
# 2. 扫描 = 点名表 + `install/` 前缀兜底 —— 凡命令行含 ``<仓库>/install/``
|
||||
# 的进程都算真机进程,以后再加新节点,不改这个文件也能被扫到;
|
||||
# 3. **按 ROS_DOMAIN_ID 区分真机与仿真**:仿真强制 77(见 sim_env.sh),
|
||||
# 真机默认 0。只按名字扫的话,停真机会顺手把正在跑的仿真的
|
||||
# robot_state_publisher 一起杀掉(两边同名、都在 conda/ROS 下)。
|
||||
#
|
||||
# 用法(在 `set -uo pipefail` 之后 source):
|
||||
# source "$(dirname "$0")/lib/real_procs.sh"
|
||||
# mapfile -t targets < <(real_proc_targets) # 只输出 PID(已排除自身/祖先/仿真)
|
||||
# real_proc_scan # "PID<TAB>域<TAB>命令行"
|
||||
# =============================================================================
|
||||
|
||||
# 仿真的域(sim_env.sh 强制那个值)。真机扫到它 = 那是仿真进程,不是残留。
|
||||
REAL_SIM_DOMAIN=77
|
||||
|
||||
_real_repo_root() {
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd
|
||||
}
|
||||
|
||||
# 兜底模式:真机安装空间。用绝对路径前缀,避免误伤 install_sim/(仿真那边)。
|
||||
# 注意 "install_sim/" **不含** 子串 "install/"(_sim 把它截断了),所以这一条
|
||||
# 天然不会扫到仿真进程。
|
||||
#
|
||||
# ⚠️ 路径要按 awk 正则转义:路径里可能有 . + - 之类字符,不转义的话
|
||||
# "." 会匹配任意字符(匹配面变大 = 误杀无关进程)。
|
||||
_real_install_dir() {
|
||||
printf '%s/install/' "$(_real_repo_root)"
|
||||
}
|
||||
|
||||
_real_regex_escape() {
|
||||
printf '%s' "$1" | sed -e 's/[][\.^$*+?(){}|\\]/\\&/g'
|
||||
}
|
||||
|
||||
# 点名表:列**不在 install/ 下**、但属于真机栈的进程。
|
||||
# 它们的可执行文件来自 conda 环境或 ROS 发行版(/opt/ros/humble/lib/...),
|
||||
# 命令行里没有仓库的 install/ 字样,兜底规则扫不到。
|
||||
#
|
||||
# ros2_control_node 控制器本体(它占着 RDK,RDK 对同一台臂是独占的)
|
||||
# controller_manager.*spawner 孤儿 spawner,会永远等一个不存在的 controller_manager
|
||||
# joint_state_publisher **mock 专用**节点;在真机上出现 = 上一轮 mock 栈的残留,
|
||||
# 它发的静态关节角会盖掉真实值
|
||||
# robot_state_publisher 发 TF 与 /robot_description;残留会让"改了描述却不生效"
|
||||
# static_transform_publisher 发 latched 的 /tf_static,残留会一直把旧外参按在新树上
|
||||
REAL_PROC_NAMED='ros2_control_node|controller_manager.*spawner|joint_state_publisher|robot_state_publisher|static_transform_publisher'
|
||||
|
||||
real_proc_patterns() {
|
||||
printf '%s|%s' "$REAL_PROC_NAMED" "$(_real_regex_escape "$(_real_install_dir)")"
|
||||
}
|
||||
|
||||
# 读某个进程的 ROS_DOMAIN_ID。读不到(僵尸/无权限/没设)输出空。
|
||||
# `|| true`:调用方可能是 `set -e` 的脚本(run_real.sh),而 tr/sed 在
|
||||
# /proc 读不到时会非零返回 —— 不兜住的话整个启动脚本会被这一行带停。
|
||||
real_proc_domain() {
|
||||
local pid="$1" v=""
|
||||
v="$(tr '\0' '\n' < "/proc/${pid}/environ" 2>/dev/null \
|
||||
| sed -n 's/^ROS_DOMAIN_ID=//p' | head -1)" || true
|
||||
printf '%s' "${v:-}"
|
||||
}
|
||||
|
||||
# 是不是仿真进程(域 == REAL_SIM_DOMAIN)。
|
||||
# 读不到 environ 时**不当**成仿真:宁可停真机时多杀一个"域未知但确实在
|
||||
# install/ 下"的进程,也不要漏掉真机残留(漏掉的代价见文件头)。
|
||||
real_proc_is_sim() {
|
||||
[ "$(real_proc_domain "$1")" = "${REAL_SIM_DOMAIN}" ]
|
||||
}
|
||||
|
||||
# 扫描:输出 "PID<TAB>域<TAB>命令行"。
|
||||
#
|
||||
# ⚠️ 两个必须踩过的坑(与 sim_procs.sh 同源,别再犯):
|
||||
# 1. `pkill -f <模式>` 会匹配到自己所在的 shell(它的命令行里就含那个模式),
|
||||
# 表现成"命令立刻返回、什么都没杀"。
|
||||
# 2. 模式串用**环境变量**传给 awk,不用 `-v pat=...`:用了 -v 的话 awk 自己的
|
||||
# 命令行里就含整串模式,于是它匹配到自己,复查时多报一个幽灵 PID。
|
||||
real_proc_scan() {
|
||||
export PAT="$(real_proc_patterns)"
|
||||
export ME="${REAL_PROC_SELF:-$$}"
|
||||
ps -eo pid=,ppid=,args= | awk '
|
||||
BEGIN { pat = ENVIRON["PAT"]; me = ENVIRON["ME"] }
|
||||
$0 ~ pat {
|
||||
if (NF < 3) next # 没有 args 字段(僵尸/内核线程)
|
||||
pid = $1; ppid = $2
|
||||
args = substr($0, index($0, $3))
|
||||
if (ppid == me) next # 本脚本的直接子进程(如 $(...) 的 fork)
|
||||
if (args ~ /real_procs\.sh/) next # 本库自己(含 grep/sed 的读数)
|
||||
if (args ~ /stop_real\.sh/) next
|
||||
if (args ~ /run_real\.sh/) next
|
||||
if (args ~ /^\s*awk/) next
|
||||
print pid "\t" args
|
||||
}'
|
||||
}
|
||||
|
||||
# 排除自己与全部祖先,跳过仿真进程,只输出 PID。
|
||||
# 设 REAL_PROC_INCLUDE_SIM=1 可以连仿真进程一起列出(自查用,停止流程不用)。
|
||||
real_proc_targets() {
|
||||
local exclude=() p=$$ pid rest _d skip e
|
||||
while [ "${p:-0}" -gt 1 ] 2>/dev/null; do
|
||||
exclude+=("$p")
|
||||
p=$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ')
|
||||
[ -z "$p" ] && break
|
||||
done
|
||||
while IFS=$'\t' read -r pid rest; do
|
||||
[ -z "${pid:-}" ] && continue
|
||||
skip=0
|
||||
for e in "${exclude[@]}"; do
|
||||
[ "$pid" = "$e" ] && skip=1 && break
|
||||
done
|
||||
[ "$skip" = "1" ] && continue
|
||||
if [ "${REAL_PROC_INCLUDE_SIM:-0}" != "1" ] && real_proc_is_sim "$pid"; then
|
||||
continue
|
||||
fi
|
||||
echo "$pid"
|
||||
done < <(real_proc_scan)
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# scripts/lib/ros_domain.sh —— 真机与仿真的 DDS 域隔离
|
||||
#
|
||||
# 为什么要单独抽一层
|
||||
# ----------------
|
||||
# 仿真侧(scripts/sim_env.sh)**强制** ROS_DOMAIN_ID=77,并且注释里写明了
|
||||
# "隔离不能靠『环境里恰好没设』"。真机侧当时没有对应保护,于是同一个坑换个
|
||||
# 方向又踩了一次(2026-09-28 实测):
|
||||
#
|
||||
# 先 source 过 sim_env.sh 的 shell(ROS_DOMAIN_ID=77 还 export 着)
|
||||
# 再起真机栈 → 真机栈落到**仿真的域**上 → 两边节点名撞车
|
||||
# (/controller_manager、/robot_state_publisher、/arm_status_node…)、
|
||||
# /joint_states 与 /tf 各有两个来源互相盖。
|
||||
#
|
||||
# 现象极具误导性:控制器 feedback 与 reference 完全一致(控制器没坏),
|
||||
# 但 TF 显示末端**一动不动** —— 顺着现象查会一路查到 IK / TF / 时钟上,
|
||||
# 全是无辜的。
|
||||
#
|
||||
# 真机侧有两个入口(scripts/env.sh 给交互 shell 用;scripts/run_real.sh 自己
|
||||
# 起 conda + install),两处都得有这个纠正,所以放在这里共用一份。
|
||||
#
|
||||
# 用法(在 `set -uo pipefail` 或 `set -eo pipefail` 之后 source):
|
||||
# source ".../lib/ros_domain.sh"
|
||||
# ros_domain_force_real
|
||||
# =============================================================================
|
||||
|
||||
# 仿真的域。改这里要同步 scripts/sim_env.sh(那边是硬编码的 77)。
|
||||
ROS_SIM_DOMAIN=77
|
||||
|
||||
# 把 ROS_DOMAIN_ID 纠正到"真机该用的域"。
|
||||
# · REAL_ROS_DOMAIN_ID 显式设了 → 用它(要换域就走这个,名字里带 REAL,
|
||||
# 不会被仿真环境的 export 撞上);
|
||||
# · 环境里是 77(仿真的域)→ **喊出来**再改回 0;
|
||||
# · 环境里没设 → 显式写 0,让人一眼能从 `env` 里看出真机跑在哪个域。
|
||||
ros_domain_force_real() {
|
||||
if [ -n "${REAL_ROS_DOMAIN_ID:-}" ]; then
|
||||
export ROS_DOMAIN_ID="${REAL_ROS_DOMAIN_ID}"
|
||||
elif [ "${ROS_DOMAIN_ID:-0}" = "${ROS_SIM_DOMAIN}" ]; then
|
||||
echo "[domain] ⚠ 环境里的 ROS_DOMAIN_ID=${ROS_SIM_DOMAIN} 是**仿真**用的域" >&2
|
||||
echo "[domain] (scripts/sim_env.sh 强制那个值)。已改回 0 起真机栈 ——" >&2
|
||||
echo "[domain] 真机与仿真同域会让节点名/话题互相盖,现象是'臂没动'" >&2
|
||||
echo "[domain] 而控制器一切正常。要另指定请设 REAL_ROS_DOMAIN_ID。" >&2
|
||||
export ROS_DOMAIN_ID=0
|
||||
elif [ -z "${ROS_DOMAIN_ID:-}" ]; then
|
||||
export ROS_DOMAIN_ID=0
|
||||
fi
|
||||
}
|
||||
Reference in New Issue
Block a user