bda2fa7129
配置(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>
134 lines
5.8 KiB
Bash
Executable File
134 lines
5.8 KiB
Bash
Executable File
#!/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
|