通止规装配:配置段进 site.yaml + 中文手册 + 成败判定「不看 $?」

配置(site.yaml → gauge_assembly:,38 键全部带中文注释)
------------------------------------------------------
- 键名/单位/类型/交叉矛盾由 gauge_config.py 校验:拼错键名报错并给最相近的候选,
  单位后缀(_deg/_mm/_m/_s/_pct/_delta)逐条对表,`contact_safe_delta ≤
  contact_tangential_delta` 这类矛盾启动就拦(R1.2:没接线的键不许静默存在)。
- 借机清掉 4 个**没接线**的键(require_preview / track_*_s / hover_m:前两个是
  stage1 那层的、后两个是 servo 那层的,放在这里等于第二份真相),
  并把 contact_speed_m_s / coarse_timeout_s / 夹爪力速 真的接上。
- site.example.yaml 由 site.yaml 生成(gen_site_example.py --check 通过)。

成败判定:`ros2 launch` 的退出码**恒为 0**
------------------------------------------
实测(Humble):子进程 exit 3,`ros2 launch` 仍返回 0。用 `$?` 判成败的脚本会把
"装配失败"记成"成功"——没人会去查一个说成功的脚本。改成:

- 节点把结论**落盘**(--result-file,默认 /tmp/gauge_assembly_result.json,原子写、
  先落盘再发话题),任何"没跑到收尾"的退出也会补一份 ok=false;
- 脚本读结论文件判成败:0=成功、1=失败、**文件不存在也算失败**并说清;
- 新增 test_gauge_result_gate.py:把脚本里那段判成败的 Python 抽出来,
  对成功/失败/干跑/缺文件/坏 JSON 五种输入断言退出码 —— 判据本身也有测试。

顺带修掉两个"看着像玄学"的现象
------------------------------
- **流程打完了终端不回提示符**:launch 里的静态 TF 发布者是常驻的,主节点退出
  带不走它 —— 挂 OnProcessExit → Shutdown。
- **"已装 1/2 孔"**:结果里 hole_index 是 0 基当前孔号,不是"装完几个",
  新增 holes_done 给人和脚本看。

文档(全部中文,含"怎么调参/还没做")
------------------------------------
- 新增 docs/GAUGE_ASSEMBLY.md:状态机全图、每步判据表、E1 掉件处理(**按实现**:
  掉件后该孔作废、换下一个孔;最后一个孔掉件则序号被夹住=重试本孔)、
  话题契约、调参顺序、验收命令、踩过的坑。
- FRAMEWORK_RULES 新增 R5.8(同一份数学只许一份 + 守卫测试)、R5.9(自检的
  期望类型与调用处同源)、R5.10(缺依赖 skip,别留常年红)、R6.9(硬退出前打
  traceback)、R6.10(ros2 launch 退出码恒 0)+ R5.7 补真机/仿真"生效方式不同"
  (真机软链即改即生效,仿真拷贝必须重编)+ 5 条症状反例、.cursor/rules 同步。
- WORKSPACE / INTERFACES / SITE_CONFIG / HANDBOOK / 两份 README 索引与决策表补齐。

验证(本机真跑)
----------------
  pytest(仿真环境)  436 passed, 14 skipped(缺 onnx 跳过)
  干跑 --sim --dry-run  → 脚本退出码 0,结论 ok=true  已装 2/2 孔
  不干跑 --sim          → 脚本退出码 1,S00 拦住,未发任何运动指令

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
arm_dev
2026-09-24 10:30:11 +08:00
parent dfc945b7e0
commit bda2fa7129
16 changed files with 741 additions and 37 deletions
+44 -1
View File
@@ -24,6 +24,10 @@
# 力阈值标定:先 bash ~/ros2/scripts/run_tactile.sh --cli 看读数量级
# 掉件重放:bash ~/ros2/scripts/run_gauge_assembly.sh 之外另开一个终端
# ros2 topic pub -1 /gauge/reload_ok std_msgs/Bool '{data: true}'
#
# 退出码:0=成功(结论文件 ok=true),1=失败(读的是结论文件,不是 ros2 launch 的
# `$?` —— 后者恒为 0,见 docs/FRAMEWORK_RULES.md R6.9)。
# 结论文件:/tmp/gauge_assembly_<SN>.json(节点写,本脚本读)
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -80,7 +84,11 @@ echo " 孔数=${HOLES:-site} 夹爪后端=${BACKEND} 干跑=${DRY_RUN}"
pkill -f 'lib/arm_application/gauge_assembly' 2>/dev/null || true
sleep 0.5
ARGS=(gripper_backend:="${BACKEND}")
# 结论落盘路径:**由本脚本决定**(节点只负责写到这里),脚本末尾再读回来判成败。
RESULT_FILE="/tmp/gauge_assembly_${SITE_ROBOT_SN}.json"
rm -f "${RESULT_FILE}"
ARGS=(gripper_backend:="${BACKEND}" result_file:="${RESULT_FILE}")
if [[ -n "${HOLES}" ]]; then ARGS+=(holes_total:="${HOLES}"); fi
if [[ "${DRY_RUN}" == true ]]; then ARGS+=(dry_run:=true); fi
@@ -88,3 +96,38 @@ echo "=== launch gauge_assembly(自检不过会自动报缺什么) ==="
# 不用 exec:exec 之后 Ctrl-C 直接打给 ros2 launch,这里的清理就没了;
# gauge_assembly 自己也会在收到 KeyboardInterrupt 时退到安全高度。
ros2 launch arm_application gauge_assembly.launch.py "${ARGS[@]}"
# ---- 判成败:**不能靠 `$?`** ----
# 实测(Humble):`ros2 launch` 的子进程 exit 3 时,ros2 launch 仍返回 0。
# 所以这里读节点落盘的结论文件 —— 否则自动化会把"装配失败"记成"成功"。
python3 - "${RESULT_FILE}" <<'PY'
import json
import sys
path = sys.argv[1]
try:
with open(path, encoding="utf-8") as fh:
res = json.load(fh)
except FileNotFoundError:
sys.exit(f"失败:没有结论文件 {path}\n"
" 节点在写出结论前就退出了(构造失败 / 崩溃 / 被 Ctrl-C)—— 看上面的日志。")
except (OSError, json.JSONDecodeError) as exc:
sys.exit(f"失败:结论文件读不了({path}):{exc}")
ok = bool(res.get("ok"))
print(f"结论:state={res.get('state')} ok={ok} "
f"已装 {res.get('holes_done')}/{res.get('holes_total')} 孔"
f"{'(干跑)' if res.get('dry_run') else ''} 掉件重放 {res.get('relocations')} 次")
if res.get("failures"):
print(" 未就绪(必检):")
for item in res["failures"]:
print(f" - {item}")
if res.get("notes"):
print(" 备注:")
for item in res["notes"]:
print(f" · {item}")
if res.get("reason"):
print(f" 原因:{res['reason']}")
if not ok:
sys.exit(f"失败:state={res.get('state')}(exit_code={res.get('exit_code', 1)})")
PY