#!/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/_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_.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