真机侧补 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:
arm_dev
2026-09-28 12:25:16 +08:00
parent bb6f792922
commit 6baf989595
10 changed files with 578 additions and 7 deletions
+63 -1
View File
@@ -410,6 +410,44 @@ conda 激活不会往 `LD_LIBRARY_PATH` 加东西,而 gz 的插件搜索路径
**更重要的是:残留进程比"没停干净"严重得多**(R4.11 解释为什么)。
脚本走两条路:进程组(主)+ 模式扫描(兜底),杀完**复查残留**并非零退出。
### R4.10b 停真机用 `scripts/stop_real.sh`(真机侧原本**没有**对应物)
R4.10/R4.11 只覆盖了仿真。真机侧长期只有 `run_real.sh` 里两行 `pkill`:
```bash
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 一条都匹配不到。
危害与 R4.11 同源 —— **冒名顶替**:残留的 adapter 照样订阅 `/trajectory`、
残留的 `joint_state_publisher` 照样发 `/joint_states`(mock 的**静态**关节角),
新一轮的请求被 DDS 负载均衡到残留的那个。
后来这四条甚至跑到**仿真的域**上,把 Gazebo 那边的 TF 盖成"静止",
现场表现为"仿真上料失败、臂摆不到竖直位姿"(详见 R4.13 的实测记录)。
所以真机侧补齐,规则与仿真侧对齐:
1. 扫描实现只有一份(`scripts/lib/real_procs.sh`),`run_real.sh` 与
`stop_real.sh` 共用 —— 不再各写一份会漂移的模式表;
2. 扫描 = 点名表 + `<仓库>/install/` 前缀兜底(以后新增节点忘了改表也能扫到);
3. **按 `ROS_DOMAIN_ID` 判别真机与仿真**:两边有同名节点
(`robot_state_publisher`、`static_transform_publisher`),只按名字扫的话
停真机会顺手把正在跑的仿真杀一半;
4. 杀完**复查残留**并非零退出(漏项要变成看得见的告警)。
⚠️ 真机侧**故意不**用仿真的"进程组"法:真机栈是前台交互跑的(要能 Ctrl-C),
没有独立进程组,它的 PGID 就是调用它的那个 shell —— 记下来再 kill 会把你自己的
终端杀掉。
### R4.11 仿真栈必须能"整组停掉":残留会伪造出"代码写错了"的现场
实测(2026-09-24)踩到的两个残留,症状与"夹爪换算写错了"**一模一样**:
@@ -470,6 +508,28 @@ elif [[ "${ROS_DOMAIN_ID:-77}" != "77" ]]; then
else export ROS_DOMAIN_ID=77; fi
```
**真机这一侧也必须同样强制**(2026-09-28 实测,反方向又踩了一次):
当时真机侧只有 `scripts/env.sh`,它**不管** `ROS_DOMAIN_ID`。于是
> 先 source 过 `sim_env.sh` 的 shell(`ROS_DOMAIN_ID=77` 还 export 着)
> 再起真机栈 → 真机栈落到**仿真的域**上。
现象极具误导性:控制器 `feedback` 与 `reference` **完全一致**(控制器没坏),
但 TF 显示末端**一动不动** —— 顺着现象查会一路查到 IK、TF、时钟上去,全是无辜的。
真正的原因是同一话题上有两个来源(真机 + 残留的 mock 栈)互相盖。
规则因此补一句:**"强制设自己的值"要成对做**。只在一侧强制,另一侧就变成了
"靠环境里恰好没设",隔离照样是运气。现在两侧都有,且都要喊出来:
| 入口 | 强制到 | 覆盖用 |
|------|--------|--------|
| `scripts/sim_env.sh`(仿真) | 77 | `SIM_ROS_DOMAIN_ID` |
| `scripts/env.sh`、`run_real.sh`、`run_sim.sh`(真机/mock) | 0 | `REAL_ROS_DOMAIN_ID` |
实现只有一份:`scripts/lib/ros_domain.sh` 的 `ros_domain_force_real`。
`run_sim.sh` 特别容易漏 —— 它名为 sim,但用的是**真机的 `install/`**、节点名与
真机完全一样,所以它属于"真机域"那一列。
---
## 5. 代码与测试
@@ -734,7 +794,7 @@ launch 会一直挂着、终端不回提示符。要显式挂 `OnProcessExit →
| 11 | 这次构建是在**目标环境**里做的吗?(`head -1 install/*/lib/*/*`) | R6.8、R5.7 |
| 12 | 我碰的文件属于"跟随当前臂"还是"按臂各留一份"?改完跑 `check_arm_prefixes.py` 了吗? | R1.1.1 |
| 13 | 我验收用的判据,是"与被测实现无关的物理量"吗?单位只在一处换算了吗? | R4.12 |
| 14 | 我起过仿真/长跑进程吗?停干净了吗(`stop_sim_gz.sh --list` 应为空)? | R4.10、R4.11 |
| 14 | 我起过仿真/真机栈/长跑进程吗?停干净了吗(`stop_sim_gz.sh --list`、`stop_real.sh --list` 都应为空)? | R4.10、R4.10b、R4.11 |
---
@@ -777,6 +837,8 @@ launch 会一直挂着、终端不回提示符。要显式挂 `OnProcessExit →
| 夹爪明明开着,`status` 一直报"开口 0 mm" | 反馈取了"虚拟宽度关节",它在 Gazebo 里根本不动 | R4.11、坑 12 |
| 验收说指尖只张开 0.13 mm(其实 127 mm) | 验收脚本自己漏了 ×1000(米 vs 毫米) | R4.12 |
| 服务/话题/TF 全在,但读数全是乱的 | 上一轮的 `sim_gripper_adapter` / `static_transform_publisher` 还活着 | R4.11 |
| 控制器 `feedback` 与 `reference` 完全一致,TF 却显示末端**一动不动** | 同样的话题上有两个来源(残留的 `joint_state_publisher` 发的**静态** mock 关节角把它盖住了)—— 别去查 IK/TF/时钟 | R4.10b、R4.13 |
| 真机栈和仿真"互相干扰"、节点名撞车 | 真机侧当时**不强制域**,`ROS_DOMAIN_ID=77` 从跑过仿真的 shell 里漏了进来 | R4.13 |
| 命令"开 20 mm",两指实际 21.75 mm,夹不住工件 | 上游 `<mimic>` 是线性近似,开口必须走正运动学反解 | R4.4、坑 12 |
| 残留进程把旧相机外参按在新仿真上 | `static_transform_publisher` 发的是 latched `/tf_static` | R4.11 |
| 同一个手眼 TF,不同 launch 发出来不一样 | 四元数换算被抄了四份,退化分支公式各不相同 | R5.8 |