真机侧补 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:
+63
-1
@@ -410,6 +410,44 @@ conda 激活不会往 `LD_LIBRARY_PATH` 加东西,而 gz 的插件搜索路径
|
|||||||
**更重要的是:残留进程比"没停干净"严重得多**(R4.11 解释为什么)。
|
**更重要的是:残留进程比"没停干净"严重得多**(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 仿真栈必须能"整组停掉":残留会伪造出"代码写错了"的现场
|
### R4.11 仿真栈必须能"整组停掉":残留会伪造出"代码写错了"的现场
|
||||||
|
|
||||||
实测(2026-09-24)踩到的两个残留,症状与"夹爪换算写错了"**一模一样**:
|
实测(2026-09-24)踩到的两个残留,症状与"夹爪换算写错了"**一模一样**:
|
||||||
@@ -470,6 +508,28 @@ elif [[ "${ROS_DOMAIN_ID:-77}" != "77" ]]; then
|
|||||||
else export ROS_DOMAIN_ID=77; fi
|
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. 代码与测试
|
## 5. 代码与测试
|
||||||
@@ -734,7 +794,7 @@ launch 会一直挂着、终端不回提示符。要显式挂 `OnProcessExit →
|
|||||||
| 11 | 这次构建是在**目标环境**里做的吗?(`head -1 install/*/lib/*/*`) | R6.8、R5.7 |
|
| 11 | 这次构建是在**目标环境**里做的吗?(`head -1 install/*/lib/*/*`) | R6.8、R5.7 |
|
||||||
| 12 | 我碰的文件属于"跟随当前臂"还是"按臂各留一份"?改完跑 `check_arm_prefixes.py` 了吗? | R1.1.1 |
|
| 12 | 我碰的文件属于"跟随当前臂"还是"按臂各留一份"?改完跑 `check_arm_prefixes.py` 了吗? | R1.1.1 |
|
||||||
| 13 | 我验收用的判据,是"与被测实现无关的物理量"吗?单位只在一处换算了吗? | R4.12 |
|
| 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 |
|
| 夹爪明明开着,`status` 一直报"开口 0 mm" | 反馈取了"虚拟宽度关节",它在 Gazebo 里根本不动 | R4.11、坑 12 |
|
||||||
| 验收说指尖只张开 0.13 mm(其实 127 mm) | 验收脚本自己漏了 ×1000(米 vs 毫米) | R4.12 |
|
| 验收说指尖只张开 0.13 mm(其实 127 mm) | 验收脚本自己漏了 ×1000(米 vs 毫米) | R4.12 |
|
||||||
| 服务/话题/TF 全在,但读数全是乱的 | 上一轮的 `sim_gripper_adapter` / `static_transform_publisher` 还活着 | R4.11 |
|
| 服务/话题/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 |
|
| 命令"开 20 mm",两指实际 21.75 mm,夹不住工件 | 上游 `<mimic>` 是线性近似,开口必须走正运动学反解 | R4.4、坑 12 |
|
||||||
| 残留进程把旧相机外参按在新仿真上 | `static_transform_publisher` 发的是 latched `/tf_static` | R4.11 |
|
| 残留进程把旧相机外参按在新仿真上 | `static_transform_publisher` 发的是 latched `/tf_static` | R4.11 |
|
||||||
| 同一个手眼 TF,不同 launch 发出来不一样 | 四元数换算被抄了四份,退化分支公式各不相同 | R5.8 |
|
| 同一个手眼 TF,不同 launch 发出来不一样 | 四元数换算被抄了四份,退化分支公式各不相同 | R5.8 |
|
||||||
|
|||||||
+16
-1
@@ -3,6 +3,15 @@
|
|||||||
实现细节在 [docs/WORKSPACE.md](../docs/WORKSPACE.md) 和 [docs/](../docs/)。
|
实现细节在 [docs/WORKSPACE.md](../docs/WORKSPACE.md) 和 [docs/](../docs/)。
|
||||||
|
|
||||||
**每天:** `run_real.sh` · `run_camera_d405.sh` · `run_visual_servo_rt.sh` · `go_home.sh`
|
**每天:** `run_real.sh` · `run_camera_d405.sh` · `run_visual_servo_rt.sh` · `go_home.sh`
|
||||||
|
停:`stop_real.sh`(**别敲 `pkill -f`** —— 先匹配到调用它的 shell,会"秒返回、什么都没停")
|
||||||
|
- 为什么单独有它:真机栈里有一族**独立于 `ros2_control_node` 存活**的进程
|
||||||
|
(`real_trajectory_adapter` / `named_pose_server` / `arm_status_node` /
|
||||||
|
`joint_state_publisher`)。它们会**冒名顶替**新一轮的栈 —— 残留的照样订阅
|
||||||
|
`/trajectory`、照样发 `/joint_states`(mock 的静态关节角),现象是
|
||||||
|
"控制器一切正常,TF 却显示臂没动"。见 `docs/FRAMEWORK_RULES.md` R4.10b。
|
||||||
|
- 它按 `install/` 前缀兜底扫,并**按 `ROS_DOMAIN_ID` 避开仿真进程**
|
||||||
|
(两边有同名的 `robot_state_publisher`)—— 所以停真机不会把正在跑的仿真杀一半。
|
||||||
|
- `--list` 只列出、不动手。
|
||||||
**三阶段串联:** `run_all.sh --arm 89 [--hole k2] [--skip-stage1]`(`run_pipeline.sh` 是它的旧名别名)
|
**三阶段串联:** `run_all.sh --arm 89 [--hole k2] [--skip-stage1]`(`run_pipeline.sh` 是它的旧名别名)
|
||||||
**手眼:** `run_handeye.sh`(`teach` / `merge` / `calibrate` / `show`)
|
**手眼:** `run_handeye.sh`(`teach` / `merge` / `calibrate` / `show`)
|
||||||
**夹爪:** `run_gripper.sh --arm 89|91` · `run_dual_gripper.sh`
|
**夹爪:** `run_gripper.sh --arm 89|91` · `run_dual_gripper.sh`
|
||||||
@@ -33,7 +42,9 @@
|
|||||||
物体的摆放(悬空/穿模/出界/互相嵌入)、场景里的像素门槛与**真机生效值**是否漂移、
|
物体的摆放(悬空/穿模/出界/互相嵌入)、场景里的像素门槛与**真机生效值**是否漂移、
|
||||||
孔检测的成像几何(亮环/孔间距换算成像素后够不够门槛)、以及粗定位用的手外相机
|
孔检测的成像几何(亮环/孔间距换算成像素后够不够门槛)、以及粗定位用的手外相机
|
||||||
能不能**完整**看见工件。第二条尤其重要:门槛抄了两份,不一致时仿真会"通过"而真机不认。
|
能不能**完整**看见工件。第二条尤其重要:门槛抄了两份,不一致时仿真会"通过"而真机不认。
|
||||||
- 注意 `run_sim.sh` 不是 Gazebo,它走 mock 驱动(假数据 + RViz),永远不会产生接触力
|
- 注意 `run_sim.sh` 不是 Gazebo,它走 mock 驱动(假数据 + RViz),永远不会产生接触力。
|
||||||
|
它用的是**真机的 `install/`**、节点名与真机完全一样,所以它跑在**真机的域 0**上
|
||||||
|
(不是仿真的 77)—— 别把它当成"另一个仿真"去和 `run_sim_gz.sh` 一起看。
|
||||||
**环境:** `env.sh` · `build_all.sh`
|
**环境:** `env.sh` · `build_all.sh`
|
||||||
**RDK:** `test_rdk_python.sh` · `rdk_switch_tool.sh` · `rdk_drag_bridge.py`
|
**RDK:** `test_rdk_python.sh` · `rdk_switch_tool.sh` · `rdk_drag_bridge.py`
|
||||||
**改注释/写文档后自检:** `python3 scripts/check_text_only.py`
|
**改注释/写文档后自检:** `python3 scripts/check_text_only.py`
|
||||||
@@ -49,6 +60,10 @@
|
|||||||
|
|
||||||
`lib/parse_arm_args.sh` 解析 `--arm 89|91`。
|
`lib/parse_arm_args.sh` 解析 `--arm 89|91`。
|
||||||
`lib/load_site_config.sh` 把 `config/site.yaml` 变成 `SITE_*`。
|
`lib/load_site_config.sh` 把 `config/site.yaml` 变成 `SITE_*`。
|
||||||
|
`lib/ros_domain.sh` 真机/仿真的 DDS 域隔离(真机 0 / 仿真 77,`ROS_DOMAIN_ID` 从仿真
|
||||||
|
环境漏进来时改回并喊出来)—— `env.sh`、`run_real.sh`、`run_sim.sh` 共用一个实现。
|
||||||
|
`lib/real_procs.sh` 真机进程扫描(点名表 + `install/` 兜底 + 按域避开仿真),
|
||||||
|
`run_real.sh` 与 `stop_real.sh` 共用一份,不会各写一份漂移的模式表。
|
||||||
|
|
||||||
## 排查脚本卡住时的常识
|
## 排查脚本卡住时的常识
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ unset _d _TRT_SITE
|
|||||||
export YOLO_AUTOINSTALL=false
|
export YOLO_AUTOINSTALL=false
|
||||||
export ROS2_WS="${ROOT}"
|
export ROS2_WS="${ROOT}"
|
||||||
|
|
||||||
|
# 真机与仿真的 DDS 隔离(细节与踩坑见 lib/ros_domain.sh 的文件头)。
|
||||||
|
# shellcheck source=lib/ros_domain.sh
|
||||||
|
source "${ROOT}/scripts/lib/ros_domain.sh"
|
||||||
|
ros_domain_force_real
|
||||||
|
|
||||||
# 主站点配置 → `SITE_*`(YOLO 权重、相机、机器人 SN……)
|
# 主站点配置 → `SITE_*`(YOLO 权重、相机、机器人 SN……)
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source "${ROOT}/scripts/lib/load_site_config.sh" 2>/dev/null || true
|
source "${ROOT}/scripts/lib/load_site_config.sh" 2>/dev/null || true
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
+25
-3
@@ -30,6 +30,10 @@ conda activate ros_flexiv
|
|||||||
source "${ROOT}/install/setup.bash"
|
source "${ROOT}/install/setup.bash"
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source "${ROOT}/scripts/lib/load_site_config.sh"
|
source "${ROOT}/scripts/lib/load_site_config.sh"
|
||||||
|
# 真机与仿真的 DDS 域隔离:这个入口**不经过** env.sh,所以这里也得纠正一次。
|
||||||
|
# shellcheck source=lib/ros_domain.sh
|
||||||
|
source "${ROOT}/scripts/lib/ros_domain.sh"
|
||||||
|
ros_domain_force_real
|
||||||
|
|
||||||
# `flexiv_hardware` 必须从 flexiv_install 加载 spdlog/RBDyn(不能用 conda 的 fmt v11)
|
# `flexiv_hardware` 必须从 flexiv_install 加载 spdlog/RBDyn(不能用 conda 的 fmt v11)
|
||||||
export LD_LIBRARY_PATH="${FLEXIV_PREFIX}/lib:${LD_LIBRARY_PATH:-}"
|
export LD_LIBRARY_PATH="${FLEXIV_PREFIX}/lib:${LD_LIBRARY_PATH:-}"
|
||||||
@@ -50,9 +54,27 @@ echo "=== catalog: ${SITE_ROBOT_CATALOG:-} ==="
|
|||||||
echo "=== handeye: ${SITE_HANDEYE_FILE:-} ==="
|
echo "=== handeye: ${SITE_HANDEYE_FILE:-} ==="
|
||||||
echo "=== camera serial: ${SITE_CAM_SERIAL:-} gripper: ${SITE_GRIP_MODEL:-} ${SITE_GRIP_PORT:-} ==="
|
echo "=== camera serial: ${SITE_CAM_SERIAL:-} gripper: ${SITE_GRIP_MODEL:-} ${SITE_GRIP_PORT:-} ==="
|
||||||
echo "=== network iface=${NET_IFACE:-auto} robot=${ROBOT_IP} pc=${SITE_PC_IP:-} ==="
|
echo "=== network iface=${NET_IFACE:-auto} robot=${ROBOT_IP} pc=${SITE_PC_IP:-} ==="
|
||||||
echo "=== stop leftover ros2_control (RDK exclusive) ==="
|
echo "=== stop leftover real stack (RDK exclusive) ==="
|
||||||
pkill -f '/controller_manager/ros2_control_node' 2>/dev/null || true
|
# 以前这里是两行 pkill(ros2_control_node + stack.launch),漏掉了
|
||||||
pkill -f 'arm_bringup.*stack.launch' 2>/dev/null || true
|
# arm_control 那一族**独立存活**的孤儿(adapter / named_pose_server /
|
||||||
|
# arm_status_node / joint_state_publisher)—— 它们会冒名顶替新一轮的栈,
|
||||||
|
# 见 lib/real_procs.sh 的文件头。扫描规则与 stop_real.sh 共用一份。
|
||||||
|
# shellcheck source=lib/real_procs.sh
|
||||||
|
source "${ROOT}/scripts/lib/real_procs.sh"
|
||||||
|
mapfile -t _stale < <(real_proc_targets)
|
||||||
|
if [ "${#_stale[@]}" != "0" ]; then
|
||||||
|
echo "清掉上一轮残留(${#_stale[@]} 个):"
|
||||||
|
for _pid in "${_stale[@]}"; do
|
||||||
|
printf ' %-7s %s\n' "$_pid" "$(ps -o args= -p "$_pid" 2>/dev/null | cut -c1-120)"
|
||||||
|
kill -TERM "$_pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
sleep 1
|
||||||
|
for _pid in "${_stale[@]}"; do
|
||||||
|
kill -0 "$_pid" 2>/dev/null && kill -KILL "$_pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
unset _pid
|
||||||
|
fi
|
||||||
|
unset _stale
|
||||||
sleep 1
|
sleep 1
|
||||||
# 清掉把 91 的 .100 误绑到 89 MAC 上的陈旧 ARP
|
# 清掉把 91 的 .100 误绑到 89 MAC 上的陈旧 ARP
|
||||||
if [[ -n "${NET_IFACE}" ]]; then
|
if [[ -n "${NET_IFACE}" ]]; then
|
||||||
|
|||||||
@@ -4,4 +4,16 @@ set -euo pipefail
|
|||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source "${ROOT}/install/setup.bash"
|
source "${ROOT}/install/setup.bash"
|
||||||
|
# mock 栈用的是**真机的** install/(不是 install_sim/),节点名与真机一模一样
|
||||||
|
# (/controller_manager、/robot_state_publisher、/arm_status_node…),
|
||||||
|
# 所以它必须待在**真机的域**里,绝不能跟着调用者的环境落到仿真的 77 上。
|
||||||
|
#
|
||||||
|
# 2026-09-28 实测就是这么踩的:这个脚本 source 的是 install/ 又**没管域**,
|
||||||
|
# 调用者的 shell 里带着 77(之前跑过仿真),于是 mock 栈落到仿真域上、
|
||||||
|
# 与 Gazebo 仿真抢 /joint_states —— 冒名顶替的残留进程把 Gazebo 那边的 TF
|
||||||
|
# 盖成"静止",现象是"仿真上料失败、臂摆不到竖直位姿",而控制器其实一切正常。
|
||||||
|
# 详见 scripts/lib/ros_domain.sh 的文件头。
|
||||||
|
# shellcheck source=lib/ros_domain.sh
|
||||||
|
source "${ROOT}/scripts/lib/ros_domain.sh"
|
||||||
|
ros_domain_force_real
|
||||||
ros2 launch arm_bringup sim.launch.py
|
ros2 launch arm_bringup sim.launch.py
|
||||||
|
|||||||
@@ -15,7 +15,15 @@
|
|||||||
# 与 scripts/run_sim.sh 的区别(两个名字容易混,这里说清):
|
# 与 scripts/run_sim.sh 的区别(两个名字容易混,这里说清):
|
||||||
# run_sim.sh ← 走 arm_bringup 的 **mock** 驱动:假数据 + RViz,无物理
|
# run_sim.sh ← 走 arm_bringup 的 **mock** 驱动:假数据 + RViz,无物理
|
||||||
# run_sim_gz.sh ← 走 Gazebo:真物理、真接触
|
# 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 的
|
# ⚠️ 这个脚本此前**不存在**,却被 install_sim_env.sh(两处)与 build_sim.sh 的
|
||||||
# 提示语引用 —— 照做会直接报"文件不存在"。现在补上。
|
# 提示语引用 —— 照做会直接报"文件不存在"。现在补上。
|
||||||
|
|||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# scripts/stop_real.sh —— 干净地停掉真机栈(Rizon4 + arm_control + arm_application)
|
||||||
|
#
|
||||||
|
# 为什么需要它(不是"能 pkill 就别写脚本")
|
||||||
|
# ----------------------------------------
|
||||||
|
# 以前真机侧没有任何停止脚本,只有 run_real.sh 里两行 pkill。而真机栈里有一族
|
||||||
|
# 进程**独立于那两条模式存活**:2026-09-28 实测抓到上一轮栈主体早已退出、只剩
|
||||||
|
# real_trajectory_adapter / named_pose_server / arm_status_node /
|
||||||
|
# joint_state_publisher
|
||||||
|
# 四条。它们会**冒名顶替**新一轮的栈:照样接 /trajectory、照样发 /arm_status、
|
||||||
|
# 照样发 /joint_states —— 新一轮的请求被 DDS 负载均衡到残留的那个,
|
||||||
|
# 现象是"指令发了臂没动"或"数据乱跳",而日志上什么都看不出来。
|
||||||
|
#
|
||||||
|
# 与之对称的仿真侧早就有 stop_sim_gz.sh(R4.10:停仿真别用 pkill -f)。
|
||||||
|
# 这个脚本把同样的纪律补给真机侧。扫描规则在 scripts/lib/real_procs.sh,
|
||||||
|
# 与 run_real.sh 共用一份,不会各写一份漂移。
|
||||||
|
#
|
||||||
|
# ⚠️ 为什么这里**不**用 sim 那套"进程组"停止法:
|
||||||
|
# run_sim_gz.sh 用 setsid 把仿真起在自己的会话里,PGID 记下来整组 kill。
|
||||||
|
# 真机栈是**前台交互**跑的(要能 Ctrl-C、要看日志),没有独立进程组;
|
||||||
|
# 它的 PGID 就是调用它的那个 shell —— 记下来再 kill 会把你自己的终端杀掉。
|
||||||
|
# 所以真机侧靠"模式扫描 + ROS_DOMAIN_ID 判别",并且杀完复查。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# bash scripts/stop_real.sh # 停真机栈(TERM,等 5s,仍在则 KILL)
|
||||||
|
# bash scripts/stop_real.sh --list # 只列出会被停掉的进程,不动手
|
||||||
|
#
|
||||||
|
# 不会动的东西:ROS_DOMAIN_ID=77 的进程(那是仿真,归 stop_sim_gz.sh 管)。
|
||||||
|
# =============================================================================
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
LIST_ONLY=0
|
||||||
|
[ "${1:-}" = "--list" ] && LIST_ONLY=1
|
||||||
|
|
||||||
|
# shellcheck source=lib/real_procs.sh
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/real_procs.sh"
|
||||||
|
|
||||||
|
mapfile -t targets < <(real_proc_targets)
|
||||||
|
|
||||||
|
if [ "${#targets[@]}" = "0" ]; then
|
||||||
|
echo "没有正在运行的真机进程(已排除本脚本自身、调用它的 shell、以及仿真域的进程)。"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "将停止以下真机进程:"
|
||||||
|
for pid in "${targets[@]}"; do
|
||||||
|
_d="$(real_proc_domain "$pid")"
|
||||||
|
printf ' %-7s [域 %s] %s\n' "$pid" "${_d:-?}" \
|
||||||
|
"$(ps -o args= -p "$pid" 2>/dev/null | cut -c1-130)"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$LIST_ONLY" = "1" ]; then
|
||||||
|
echo "(--list:未动手。要连仿真进程一起看:REAL_PROC_INCLUDE_SIM=1 bash scripts/stop_real.sh --list)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 先 TERM:ros2_control_node 要机会去把 RDK 的会话放掉、把臂置于安全状态。
|
||||||
|
for pid in "${targets[@]}"; do
|
||||||
|
kill -TERM "$pid" 2>/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
# 为什么等:RDK / flexiv_hardware 的析构不是瞬时的(要停实时线程、放 socket)。
|
||||||
|
# 立刻 KILL 有可能把 RDK 会话留在"半开"状态,下一次连接要等它超时。
|
||||||
|
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
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 复查:杀完再扫一遍。
|
||||||
|
# 这一步是为了让"表漏了某个进程"变成**看得见的告警**,而不是
|
||||||
|
# "脚本说已停止、实际有残留"。上面那四条孤儿当初就是这么漏掉的。
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
sleep 0.5
|
||||||
|
mapfile -t leftover < <(real_proc_targets)
|
||||||
|
|
||||||
|
if [ "${#leftover[@]}" != "0" ]; then
|
||||||
|
echo "⚠ 仍有真机相关进程存活(说明它既不在 lib/real_procs.sh 的点名表里,"
|
||||||
|
echo " 也不在 install/ 下 —— 请把它加进点名表,或让它从 install/ 启动):"
|
||||||
|
for pid in "${leftover[@]}"; do
|
||||||
|
printf ' %-7s %s\n' "$pid" "$(ps -o args= -p "$pid" 2>/dev/null | cut -c1-130)"
|
||||||
|
done
|
||||||
|
echo "真机栈已停止,但有残留 —— 残留会与下一次启动抢话题/TF。"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "真机栈已停止。"
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"""真机/仿真的 DDS 域隔离与真机进程扫描(scripts/lib/ros_domain.sh、real_procs.sh)。
|
||||||
|
|
||||||
|
为什么这两个 shell 库值得用 pytest 钉住
|
||||||
|
--------------------------------------
|
||||||
|
2026-09-28 无人值守实测踩了一次,代价是"仿真上料失败"查到 IK/TF/时钟上去:
|
||||||
|
先 source 过 sim_env.sh 的 shell 里(ROS_DOMAIN_ID=77 还 export 着)再起真机栈,
|
||||||
|
真机栈就落到**仿真的域**上,两边节点名撞车、/joint_states 与 /tf 各有两份来源
|
||||||
|
互相盖。现象是"控制器 feedback 与 reference 完全一致(控制器没坏),但 TF 显示
|
||||||
|
末端一动不动"。
|
||||||
|
|
||||||
|
仿真侧早就强制 77 并写明了"隔离不能靠『环境里恰好没设』",真机侧没有对应保护。
|
||||||
|
ros_domain.sh 补上了保护;这个文件钉住它的**四种输入 + 一个不许误报**,
|
||||||
|
以及 real_procs.sh 的域判别(否则停真机会顺手杀掉正在跑的仿真)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
LIB = ROOT / "scripts" / "lib"
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
os.name != "posix", reason="这两个库是 bash + /proc,只在 Linux 上有意义"
|
||||||
|
)
|
||||||
|
|
||||||
|
#: conda 里的 bash 在 `bash -c` 时会往 stderr 打这一条,和被测逻辑毫无关系。
|
||||||
|
#: 不滤掉的话,"非 77 的域不该有任何提示"那条用例会被它永久污染,最后只能把
|
||||||
|
#: 断言削弱或删掉 —— 而"不许误报"和"必须报"一样是要钉的性质。
|
||||||
|
_LIBTINFO_NOISE = "no version information available"
|
||||||
|
|
||||||
|
|
||||||
|
def _stderr_clean(p: subprocess.CompletedProcess) -> str:
|
||||||
|
return "\n".join(
|
||||||
|
ln for ln in p.stderr.splitlines() if _LIBTINFO_NOISE not in ln
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_env(**overrides: object) -> dict:
|
||||||
|
"""干净环境:先摘掉三个域变量,再套用 overrides。
|
||||||
|
|
||||||
|
跑测试的那个 shell 里的 ROS_DOMAIN_ID 经常正好是 77(因为常跑仿真),
|
||||||
|
不清掉的话用例会永远落在同一个分支上 —— 这正是被测代码要防的那类错误。
|
||||||
|
"""
|
||||||
|
env = dict(os.environ)
|
||||||
|
for k in ("ROS_DOMAIN_ID", "REAL_ROS_DOMAIN_ID", "SIM_ROS_DOMAIN_ID"):
|
||||||
|
env.pop(k, None)
|
||||||
|
env.update({k: str(v) for k, v in overrides.items()})
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _run_sourced(snippet: str, env_overrides: dict) -> subprocess.CompletedProcess:
|
||||||
|
"""source ros_domain.sh 后执行 snippet,返回 CompletedProcess。"""
|
||||||
|
return subprocess.run(
|
||||||
|
["bash", "-c", f'source "{LIB}/ros_domain.sh"\nros_domain_force_real\n{snippet}'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=_clean_env(**env_overrides),
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sim_domain_leak_is_corrected_to_zero_and_announced() -> None:
|
||||||
|
"""核心用例:环境里带着仿真的 77 → 必须改回 0,并且**喊出来**。
|
||||||
|
|
||||||
|
只改不说不行:现场会以为"我明明设过域"而找不到原因。
|
||||||
|
"""
|
||||||
|
r = _run_sourced("echo -n $ROS_DOMAIN_ID", {"ROS_DOMAIN_ID": 77})
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
assert r.stdout == "0", f"77 是仿真的域,真机栈不能落在上面;实际 {r.stdout!r}"
|
||||||
|
assert "77" in r.stderr and "仿真" in r.stderr, (
|
||||||
|
"改掉别人的环境必须留一句人话,否则现场不知道域被换过"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unset_domain_becomes_explicit_zero() -> None:
|
||||||
|
"""没设 → 显式写 0(而不是留空)。
|
||||||
|
|
||||||
|
显式写出来的意义:`env | grep ROS_DOMAIN_ID` 能一眼看出真机跑在哪个域,
|
||||||
|
不用去记"没设等于 0"。
|
||||||
|
"""
|
||||||
|
r = _run_sourced("echo -n $ROS_DOMAIN_ID", {})
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
assert r.stdout == "0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_real_domain_override_wins() -> None:
|
||||||
|
"""REAL_ROS_DOMAIN_ID 是换域的唯一出口(名字带 REAL,不会被仿真环境撞上)。"""
|
||||||
|
r = _run_sourced("echo -n $ROS_DOMAIN_ID", {"REAL_ROS_DOMAIN_ID": 5})
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
assert r.stdout == "5"
|
||||||
|
|
||||||
|
|
||||||
|
def test_other_domain_is_left_alone_and_not_announced() -> None:
|
||||||
|
"""用户自己挑的域(非 77)不许被动、也不许刷警告。
|
||||||
|
|
||||||
|
"不许误报"和"必须报"一样重要:每次 source env.sh 都刷一行吓人的警告,
|
||||||
|
人就会开始无视它 —— 真出事那次也就看不见了。
|
||||||
|
"""
|
||||||
|
r = _run_sourced("echo -n $ROS_DOMAIN_ID", {"ROS_DOMAIN_ID": 3})
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
assert r.stdout == "3"
|
||||||
|
assert _stderr_clean(r) == "", f"非 77 的域不该有任何提示;实际 {_stderr_clean(r)!r}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_procs_patterns_cover_install_space_and_named_nodes() -> None:
|
||||||
|
"""扫描规则要同时覆盖两半:install/ 兜底 + 不在 install/ 下的点名表。
|
||||||
|
|
||||||
|
只留兜底会漏 robot_state_publisher / joint_state_publisher(它们在
|
||||||
|
conda 或 /opt/ros 下,命令行里没有仓库路径);只留点名表会漏
|
||||||
|
arm_control 那一族孤儿(2026-09-28 实测漏的就是它们)。
|
||||||
|
"""
|
||||||
|
r = subprocess.run(
|
||||||
|
[
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
f'source "{LIB}/real_procs.sh"\nreal_proc_patterns',
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
pat = r.stdout
|
||||||
|
# 兜底:仓库的 install/(而不是笼统的 install)
|
||||||
|
assert f"{ROOT}/install/" in pat
|
||||||
|
# 点名表里的关键项
|
||||||
|
for name in ("ros2_control_node", "joint_state_publisher", "robot_state_publisher"):
|
||||||
|
assert name in pat, f"{name} 不在扫描规则里"
|
||||||
|
# 关键性质:install_sim/ **不**该被这条兜底模式匹配到(否则停真机会杀仿真)
|
||||||
|
escaped = f"{ROOT}/install/"
|
||||||
|
assert f"{ROOT}/install_sim/arm_control/lib/arm_control/arm_status_node".find(escaped) == -1
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_procs_domain_of_own_process_is_readable() -> None:
|
||||||
|
"""real_proc_domain 要能从 /proc 读出进程的域(域判别的地基)。
|
||||||
|
|
||||||
|
这条地基不牢,real_proc_is_sim 就会恒为假 —— 于是 stop_real.sh 会把正在跑的
|
||||||
|
仿真的 robot_state_publisher 一起杀掉(两边同名)。那种误杀的表现是"仿真莫名
|
||||||
|
少了大半 TF",比漏杀更难查。
|
||||||
|
|
||||||
|
⚠️ 域必须通过 `env` 在 **exec 时**给:/proc/<pid>/environ 读的是进程启动那一刻
|
||||||
|
的环境块,进去之后再 `export ROS_DOMAIN_ID=42` 是**改不到**它的。
|
||||||
|
(第一版用例就是这么写的,拿到的是父 shell 的值,于是它测的不是被测代码。)
|
||||||
|
"""
|
||||||
|
env = _clean_env()
|
||||||
|
env["ROS_DOMAIN_ID"] = "42"
|
||||||
|
r = subprocess.run(
|
||||||
|
["bash", "-c", f'source "{LIB}/real_procs.sh"\nreal_proc_domain $$'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
assert r.stdout.strip() == "42", f"读到的域不对:{r.stdout!r}(stderr={_stderr_clean(r)!r})"
|
||||||
Reference in New Issue
Block a user