Files
ros_contorl/scripts/run_gauge_assembly.sh
T
arm_dev 55e5f8f20b --sim 时按仿真台别名导出手眼真值:--arm 89 --sim 照着文档敲必然失败
`--arm 89 --sim` 是个**合法且有意的**用法(run_gauge_assembly.sh 里
_HAD_ARM 那段写着"想用仿真核对某台真机的点位时会这么用"),但手眼真值导出
传的是 ${ROBOT_SN} —— 于是帧名成了 Rizon4-063989_flange,而仿真 URDF 里是
sim_flange。导出先干等 20s,再报"仿真栈起来了吗?",把人引去重启一个明明在
跑的仿真。文档 §7 里写的就是这条命令,所以照着敲 100% 失败。

真值必须按**仿真台**的帧名导出,与"用哪台臂的点位"无关 → 固定用仿真台别名
(SIM_ARM_ALIAS 可覆盖,默认 sim)。

顺带把 §7 那组验收命令补成真能跑的:
  · 以前只写"先编",没说**怎么起仿真**,也没说**起之前要清干净** ——
    这次就是在这两步上栽的,新增 §6.5 把三步(编 / 清 / 起)写全;
  · "arm_application 子集 454 passed"配的那条裸 `PYTHONPATH=... pytest`
    命令实测已经收集阶段就 error(导入不到 ament),改成走 run_unit_tests.sh
    并更新数字;总测试数 1118 → 1128。

验证:`bash scripts/run_gauge_assembly.sh --arm 89 --sim --dry-run` 现在
state=DONE ok=True 2/2 孔、退出码 0(修改前必然退出码 1)。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 12:25:26 +08:00

205 lines
11 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"
# 用户**这一条命令**里有没有显式指定臂?要看原始 argv:
# `parse_arm_args` 只设 ROBOT_SN,而调用者的 shell 可能已经 export 了 ROBOT_SN
#(比如先 source 过 env.sh)—— 那种情况下"仿真要不要默认成 sim 台"就分不清了。
_HAD_ARM=false
for _a in "$@"; do
if [[ "${_a}" == "--arm" || "${_a}" == "--sn" ]]; then _HAD_ARM=true; fi
done
unset _a
# 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 && "${_HAD_ARM}" == false ]]; then
# 仿真默认选「仿真台」——就是 site.yaml 里 robots[] 的那条 `sn: sim`
#(TF/关节名是 sim_* 前缀,相机是 sim_d405)。**必须在 source sim_env.sh 之前**:
# 那里会把 site.yaml 导成 SITE_*,之后再改就晚了。
# 显式 --arm 仍然优先:想用仿真核对某台真机的点位时会这么用。
export ROBOT_SN=sim
export ROBOT_ARM=sim
fi
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} 仿真时钟=${USE_SIM}"
# 清掉上一轮可能残留的装配节点(同一时间只允许一个进程在指挥这条臂)
# 不 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
# 仿真必须 use_sim_time:=true:本流程的"帧新不新"和 TF 查询都按节点时钟比对。
# 忘了带的现场是"相机 13Hz 明明在发,S00 却报腕部相机无数据(过期 1.79e9s)" ——
# 报出来的原因是假的,人只会去查相机。
if [[ "${USE_SIM}" == true ]]; then ARGS+=(use_sim_time:=true); fi
# 仿真里手眼是"URDF 的真值导出成文件"(不标定)。没有这份文件,S04 的
# visual_servo 会直接拒绝启动("手眼标定结果缺失"),整条流程卡在 S04↔S03 循环。
# 所以这里先把它刷成当前真值(已是最新则什么都不做)。
# 手眼静态 TF 由 URDF 提供 → 本流程**不**发(否则同一对父子两个发布者)。
if [[ "${USE_SIM}" == true ]]; then
ARGS+=(publish_handeye_tf:=false)
echo "=== 刷新仿真手眼真值(sim_eye_in_hand.yaml / sim_d435_eye_to_hand.yaml) ==="
# ⚠️ 这里**必须**用仿真台的别名,不能用 ${ROBOT_SN}。
# 真值要按**仿真 URDF 的帧名**导出(sim_flange / sim_d405_color_optical_frame)。
# 而 `--arm 89 --sim` 是个**合法且有意的**用法(见上面 _HAD_ARM 那段:
# "想用仿真核对某台真机的点位时会这么用")——那时 ROBOT_SN=Rizon4-063989,
# 帧名就成了 Rizon4-063989_flange,仿真里根本没有这个帧:
# 导出会先等 20s 再报"仿真栈起来了吗?",把人引去重启一个明明在跑的仿真。
# 实测(2026-09-28)就是这么踩的 —— 文档 §7 里写的就是 `--arm 89 --sim`,
# 照着敲必然失败。
if ! python3 "${ROOT}/scripts/gen_sim_handeye.py" --arm "${SIM_ARM_ALIAS:-sim}"; then
echo "ERROR: 仿真手眼真值导出失败(上面有原因)。仿真栈起来了吗?" >&2
echo " bash scripts/run_sim_gz.sh 之后重试。" >&2
exit 1
fi
# 仿真没有"人把规塞进夹爪"这一步:不装规,整条流程会在 S07 才暴露
#(下了 25mm 什么也没碰到,看着像力阈值问题)。所以这里上前先上料,
# 装不上就直接停 —— 不允许带着空夹爪往下跑。
# 真机不需要:现场由人装夹,S02 的夹持确认是预留跳过项(grip_check)。
echo "=== 仿真上料:把通止规装进夹爪(真机由人工装夹) ==="
if ! python3 "${ROOT}/scripts/sim_load_gauge.py"; then
echo "ERROR: 仿真上料失败(上面有原因)。可以单独重跑:" >&2
echo " python3 scripts/sim_load_gauge.py # 装规" >&2
echo " python3 scripts/sim_load_gauge.py --dry-run # 只看算出来的位姿" >&2
exit 1
fi
# 工具长**用实测值**,不用 site.yaml 的标称值(2026-09-24 的教训,完整说明见
# sim_load_gauge._write_measured_geometry):仿真里规会在指间下滑到落座位置,
# 而下滑量随装夹变化(实测见过 243 / 254mm)。工具长写小了,二阶段悬停就会把
# 规尖停在工件**里面** —— 接着 S05 的横向对准变成拿规尖拨工件(工件被推走、
# 残差恒不收敛),日志却报"检查手眼/相机安装";写大了则 S07 提前判"台肩落面"。
# 真机没有这个文件(现场装夹后把实测值写进 site.yaml),所以这条只在仿真里走。
GEOM_FILE="${ROOT}/devices/sim/gazebo/config/sim_tool_geometry.yaml"
if [[ -f "${GEOM_FILE}" ]]; then
TOOL_LEN="$(python3 -c 'import sys,yaml;print(yaml.safe_load(open(sys.argv[1],encoding="utf-8"))["tool_tip_below_flange_m"])' "${GEOM_FILE}")"
if [[ -n "${TOOL_LEN}" ]]; then
ARGS+=(tool_tip_below_flange_m:="${TOOL_LEN}")
echo "=== 工具长用**实测值** ${TOOL_LEN}m(${GEOM_FILE#"${ROOT}/"})==="
fi
else
echo "⚠ 没有 ${GEOM_FILE#"${ROOT}/"}:工具长退回 site.yaml 的标称值。" >&2
echo " 上料脚本每次都会重写它 —— 没有这个文件说明上料没走完(或没落盘)。" >&2
fi
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