Files
ros_contorl/scripts/run_gauge_assembly.sh
T
arm_dev bda2fa7129 通止规装配:配置段进 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>
2026-09-24 10:30:11 +08:00

134 lines
5.8 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 通止规装配(看板 S00–S11):自检 → 回 home+极限角 → 跟踪 → 定位 → 悬停 →
# 柔顺接触 → 棘轮拧入 → 反向拧出 → 换孔 → 复位。
#
# 前置(本脚本**不**替你起这些,只让 S00 自检告诉你缺什么):
# T1 bash ~/ros2/scripts/run_real.sh --arm 89 # 机械臂控制器 + /home
# T2 bash ~/ros2/scripts/run_camera_d405.sh --arm 89 # 腕部 D405 预览
# T2 bash ~/ros2/scripts/run_tactile.sh # Orisys 触觉三向力
# T3 bash ~/ros2/scripts/run_gripper.sh # 夹爪服务(张开/夹紧/状态)
# —— 手眼标定产物 handeye/results/<SN>_eye_in_hand.yaml(S05 定位的前提)
#
# 为什么不在脚本里检查/拉起这些:判断标准只能有一处。脚本里查一遍、节点里
# 再查一遍,就会出现"脚本说没问题、节点说没数据"(发布者不在的假阳性),
# 而 S00 的报告是打印给人看的,带"缺了会怎样"。
#
# 用法:
# bash ~/ros2/scripts/run_gauge_assembly.sh --arm 89 # 完整装配(2 个孔)
# bash ~/ros2/scripts/run_gauge_assembly.sh --arm 89 --holes 1 # 只装 k1
# bash ~/ros2/scripts/run_gauge_assembly.sh --arm 89 --dry-run # 只验编排/配置,不动臂
# bash ~/ros2/scripts/run_gauge_assembly.sh --sim --dry-run # 仿真环境跑干跑
# bash ~/ros2/scripts/run_gauge_assembly.sh --arm 89 --backend legacy # 强制真机夹爪后端
#
# 参数接口:config/site.yaml → gauge_assembly(力阈值/角度/轮数/超时全部可调)
# 力阈值标定:先 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)"
USE_SIM=false
DRY_RUN=false
HOLES=""
BACKEND="auto"
# shellcheck disable=SC1091
source "${ROOT}/scripts/lib/parse_arm_args.sh"
parse_arm_args "$@"
set -- "${PARSE_ARM_REST[@]}"
while [[ $# -gt 0 ]]; do
case "$1" in
--sim) USE_SIM=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--holes) HOLES="${2:?}"; shift 2 ;;
--backend) BACKEND="${2:?}"; shift 2 ;;
-h|--help) sed -n '2,26p' "$0"; exit 0 ;;
*) echo "Unknown: $1" >&2; exit 1 ;;
esac
done
case "${BACKEND}" in
auto|contract|legacy) ;;
*) echo "--backend 只能是 auto|contract|legacy,收到 ${BACKEND}" >&2; exit 1 ;;
esac
if [[ "${USE_SIM}" == true ]]; then
echo "=== 仿真环境(install_sim + ROS_DOMAIN_ID=77) ==="
# shellcheck disable=SC1091
source "${ROOT}/scripts/sim_env.sh"
else
# shellcheck disable=SC1091
source "${ROOT}/scripts/env.sh"
fi
if [[ -z "${SITE_ROBOT_SN:-}" ]]; then
echo "ERROR: 没选出机械臂。用 --arm 89 指定,或填 config/site.yaml 的 robot.sn" >&2
exit 1
fi
if [[ -n "${SITE_FASTDDS_PROFILE:-}" && -f "${SITE_FASTDDS_PROFILE}" ]]; then
export FASTRTPS_DEFAULT_PROFILES_FILE="${SITE_FASTDDS_PROFILE}"
export RMW_IMPLEMENTATION="${RMW_IMPLEMENTATION:-rmw_fastrtps_cpp}"
fi
echo "=== 通止规装配 arm=${SITE_ROBOT_ARM:-?} ${SITE_ROBOT_LABEL:-} (${SITE_ROBOT_SN}) ==="
echo " 孔数=${HOLES:-site} 夹爪后端=${BACKEND} 干跑=${DRY_RUN}"
# 清掉上一轮可能残留的装配节点(同一时间只允许一个进程在指挥这条臂)
# 不 pkill 整个 arm_application:会连相机/触觉服务一起带走(那些是本脚本的前置)
pkill -f 'lib/arm_application/gauge_assembly' 2>/dev/null || true
sleep 0.5
# 结论落盘路径:**由本脚本决定**(节点只负责写到这里),脚本末尾再读回来判成败。
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
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