Files
ros_contorl/scripts/run_gauge_assembly.sh
T
arm_dev c1cd862eea 真机落地前的仿真补齐:夹持/腕力/真值位姿,让仿真能验证 S07–S09
这批是 09-24 那轮仿真工作的收尾,一直没提交。内容按目的分四块:

1) 仿真真能「夹住」工件
   sim_grasp_lock:给规与夹爪之间加接触锁(默认关,run_gauge_assembly --sim 才开)。
   没有它时仿真里规只靠摩擦,S08 棘轮一转就滑出指间,于是把「仿真不真」误读成
   「力判据不对」。新增 test_sim_grasp_lock.py。

2) 腕部力传感器进仿真
   sim_wrist_ft(+ config/wrist_ft.yaml):S07 的首选判据是腕部 FT 的轴向突变,
   而仿真里以前根本没有腕部力,S07 只能退到触觉兜底,两套判据从没在同一场景下比过。
   现在两条通道都在,接触判定可对照。

3) 真值位姿可查(不再靠猜)
   sim_live_pose / sim_pose_service:把 Gazebo 里刚体的实测位姿发布出来,
   stage2 的真值路径按**实测**位姿算孔位,而不是按场景文件里的标称位姿。
   顺带把真值孔位的推导(truth_holes)独立出来并补测。

4) 工具几何成为可配置项
   config/sim_tool_geometry.yaml + stage2 的 tool_tip_below_flange_m 启动参数:
   仿真里规会在指间下滑,工具长每次装夹都不同(实测见过 243/254mm),
   悬停高度与 S07 下探预算必须按实测值算,不能再写死。

应用层同时跟进:gauge_fsm / gauge_config 把「悬停时工具尖端入没入工件」的判据
(hover_z_guard)与拖件判定(drag_verdict)收成纯函数并补测;stage3 搜孔、
site 导出、run_gauge_assembly 与若干文档同步。
新增 test_gauge_exit_lift / test_gauge_gripper_client / test_node_handler_collisions。

测试:全仓库 1085 passed。
2026-09-28 10:17:06 +08:00

197 lines
9.9 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) ==="
if ! python3 "${ROOT}/scripts/gen_sim_handeye.py" --arm "${ROBOT_SN:-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