From baf7601995e3f85eff6ba0e7696159048155f823 Mon Sep 17 00:00:00 2001 From: arm_dev Date: Mon, 28 Sep 2026 12:25:20 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=84=E6=A3=80=E8=A1=A5=E4=B8=8A=20arm=5Fco?= =?UTF-8?q?ntrol=20=E5=AD=A4=E5=84=BF=E4=B8=8E=20mock=20=E7=9A=84=20joint?= =?UTF-8?q?=5Fstate=5Fpublisher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一轮残留的那四条孤儿(real_trajectory_adapter / named_pose_server / arm_status_node / joint_state_publisher)在预检里**一条都报不出来**: 老的六条模式只覆盖 ros2_control_node 和 stack.launch,而它们的特征恰恰是 "栈的主体已经没了、只剩它们",所以两条老模式全都匹配不到。 补两条: · `install/arm_control/lib/arm_control/` —— 一次覆盖该包全部节点 (用 install/ 而不是 install_sim/:仿真同名节点不该被真机体检误报, "install_sim/" 不含子串 "install/",这条性质用测试钉住了); · `joint_state_publisher` —— mock 专用节点,真机上出现 = 上一轮 mock 栈的 残留。它发的**静态** /joint_states 会盖掉真实关节角,是"控制器一切正常、 TF 显示臂没动"这个假象的元凶,所以说明里把它写清楚。 人工动作也改成先指向新的 stop_real.sh(它真能带走这些孤儿),保留手动 pkill 兜底。 新增 3 项单测(含"不许误报 install_sim/"的负向用例)。 Co-authored-by: Cursor --- scripts/lib/real_preflight.py | 30 +++++++++++- .../test/test_real_preflight.py | 47 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/scripts/lib/real_preflight.py b/scripts/lib/real_preflight.py index 749ee0b..d434e5c 100644 --- a/scripts/lib/real_preflight.py +++ b/scripts/lib/real_preflight.py @@ -68,6 +68,29 @@ LEFTOVER_PATTERNS: Tuple[Tuple[str, str], ...] = ( ("arm_application/tactile", "触觉服务还在跑(V4L 独占)"), ("dh_pgea", "夹爪节点还在跑(串口独占)"), ("controller_manager/spawner", "孤儿 spawner 还在等 controller_manager"), + # ↓ 2026-09-28 实测补的:这一族**独立于** ros2_control_node 存活。 + # `ros2 launch` 被 Ctrl-C / 崩掉时,子进程不一定跟着退;而下面这几条 + # 恰恰是"栈的主体已经没了、只剩它们"的那个状态 —— 上面两条模式全都 + # 匹配不到(`ros2_control_node` 已退、`stack.launch` 的命令行也没了)。 + # 留着的危害不是"占资源",而是**冒名顶替**:残留的 + # real_trajectory_adapter 照样订阅 /trajectory、named_pose_server 照样 + # 提供 /set_named_pose、arm_status_node 照样发 /arm_status,于是新一轮 + # 的请求被 DDS 负载均衡到"残留的那个",日志上看不出任何异常,现象是 + # "指令发了、臂没动"或"状态跳来跳去"。仿真侧早在 sim_procs.sh 的注释里 + # 记过同一个坑(两个适配器同时跑 → 开口恒为 0mm),真机这边的表当时没同步。 + # 用 `install/`(不是 install_sim/)前缀:仿真的同名节点在 install_sim/ 下, + # 不该被真机体检误报。 + ( + "install/arm_control/lib/arm_control/", + "arm_control 的节点还在跑(trajectory_bridge / real_trajectory_adapter / " + "named_pose_server / arm_status_node 会继续接发 /trajectory、/set_named_pose、" + "/arm_status,新一轮的调用可能被发给残留的那个)", + ), + ( + "joint_state_publisher", + "joint_state_publisher 还在跑(**mock 专用**节点:它发的 /joint_states 会盖掉" + "真实关节角,TF 看着像'臂根本没动',而控制器那边其实一切正常)", + ), ) @@ -196,8 +219,11 @@ def check_leftovers( LEVEL_WARN, ["上一轮有进程没退干净,这一轮的现象会指向别处:"] + [f" · {h}" for h in hits], actions=[ - "先清干净再起栈:pkill -f '[r]os2_control_node';" - "pkill -f '[a]rm_bringup.*stack.launch';pkill -f '[s]pawner'" + "先清干净再起栈:bash scripts/stop_real.sh" + "(按 install/ 前缀兜底扫 + 按 ROS_DOMAIN_ID 避开仿真进程,能带走上面这些孤儿;" + "没有它时手动清:pkill -f '[r]os2_control_node';" + "pkill -f '[a]rm_bringup.*stack.launch';pkill -f '[s]pawner';" + "pkill -f '[a]rm_control/lib/arm_control';pkill -f '[j]oint_state_publisher')" ], ) diff --git a/src/arm_application/test/test_real_preflight.py b/src/arm_application/test/test_real_preflight.py index 49e5616..8d06d11 100644 --- a/src/arm_application/test/test_real_preflight.py +++ b/src/arm_application/test/test_real_preflight.py @@ -113,6 +113,53 @@ def test_leftovers_does_not_match_its_own_command_line() -> None: assert c.level == pf.LEVEL_OK +def test_leftovers_reports_arm_control_orphans() -> None: + """2026-09-28 实测:栈主体早没了,只剩 arm_control 这一族**独立存活**的孤儿。 + + 上面两条老模式(ros2_control_node / stack.launch)一条都匹配不到它们, + 而它们的危害是**冒名顶替**:照样接 /trajectory、照样发 /arm_status。 + """ + ps = ( + " 100 /home/cc/anaconda3/envs/ros_flexiv/bin/python3.11 " + "/home/cc/ros2/install/arm_control/lib/arm_control/real_trajectory_adapter\n" + " 200 /home/cc/anaconda3/envs/ros_flexiv/bin/python3.11 " + "/home/cc/ros2/install/arm_control/lib/arm_control/arm_status_node\n" + " 300 /usr/bin/htop\n" + ) + c = check_leftovers(ps) + assert c.level == pf.LEVEL_WARN, "孤儿 adapter/status 必须报出来" + assert any("arm_control" in ln for ln in c.lines) + assert any("发给残留的那个" in ln for ln in c.lines), ( + "要说清危害是'新一轮请求被发给残留的那个',而不是含糊的'有残留'" + ) + + +def test_leftovers_flags_mock_joint_state_publisher() -> None: + """joint_state_publisher 是 **mock 专用**:真机上出现 = 上一轮 mock 栈的残留。 + + 它发的静态 /joint_states 会盖掉真实关节角 —— 控制器 feedback 与 reference + 完全一致(控制器没坏),TF 却显示末端一动不动。这个假象极难查, + 所以必须点名。 + """ + ps = " 200 /usr/bin/python3 /opt/ros/humble/lib/joint_state_publisher/joint_state_publisher\n" + c = check_leftovers(ps) + assert c.level == pf.LEVEL_WARN + assert any("joint_state_publisher" in ln for ln in c.lines) + + +def test_leftovers_ignores_sim_install_prefix() -> None: + """仿真的同名节点在 install_sim/ 下,不该被真机体检误报。 + + "install_sim/" 不含子串 "install/"(_sim 把它截断了),这条断言就是钉住 + 这个性质 —— 哪天有人把模式改成笼统的 "install",真机会对着仿真一直喊残留。 + """ + ps = ( + " 100 /home/cc/ros2/install_sim/arm_control/lib/arm_control/arm_status_node\n" + ) + c = check_leftovers(ps) + assert c.level == pf.LEVEL_OK + + def test_network_ok_when_ping_works() -> None: c = check_network(0, "3 received", [], "192.168.2.101") assert c.level == pf.LEVEL_OK and not c.actions