Files
ros_contorl/scripts/run_handeye.sh
T
arm_dev 72353f3773 注释与提示信息全面中文化:源码 66 个 .py + 脚本/配置 45 个文件
背景:现场读代码的人只读中文。此前代码里的 docstring 是英文、行内注释是中文,
一半英文一半中文反而更难看懂 —— 新手打开一个 .py 不知道这个模块为什么存在。

范围(本仓库自己的代码,厂商上游不动)
  src/arm_application/**   37 个文件:servo_node / perception_node / handeye_* / dh_pgea /
                           yolo_detector / kalman_filter / goto_ee_plane / dual_gripper …
  src/arm_interfaces/**    11 个文件:paths / manifest / loader / config_guard / errors …
  src/arm_control/**        5 个文件
  src/arm_bringup/**        3 个 launch
  scripts/**                6 个 .py + 29 个 .sh(scripts/ 是"唯一操作入口",用户屏幕上
                           看到的就是这些提示,所以提示信息也一并中文化)
  config/ handeye/ devices/ src/arm_*/config/   17 个 .yaml/.xml/.xacro/.cfg

口径(写进 docs/FRAMEWORK_RULES.md 新增的 R6.5,作为长期约束)
  注释、docstring、print/日志/异常里给人看的句子 = 中文;
  标识符、API 名、话题名、参数名、字段名、命令名 = 保留英文原文(用 `` `` 包起来);
  单位、十六进制、协议报文、CRC 值、命令示例 = 原样保留;
  src/flexiv_*/ = 厂商上游代码,不许改也不许翻译(R4.4)。
  翻译保原意与原有详细程度:那些"为什么不能用另一种做法""踩过什么坑"的说明
  全部完整保留 —— 它们才是这份代码里最贵的部分。

新增 scripts/check_text_only.py:只改文字时的自证工具
  人工 review 靠不住(115 个文件、3000 多行 diff)。这个脚本把改动前后的源码解析成
  语法树,删掉 docstring、抹平字符串常量、把 f-string 只留"插值了几次 + 插值是什么",
  再比对:完全相同 = 改动范围内没有逻辑变化。
  留了一个口子:f-string 的插值按内容排序后再比 —— 因为中文语序常常要和英文反着来
  (f"on {port} read {idx} failed" → f"在 {port} 上读 {idx} 失败"),只换显示顺序不该判失败;
  但多插一个、少插一个、换掉被插入的表达式,立刻会报出来。
  本次 66 个 .py 全部通过该闸门。

验收
  改动的 66 个 .py:scripts/check_text_only.py 全部通过(零逻辑变化)
  45 个非 .py 文件:去注释后与 HEAD 逐字节相同(零配置变化)
  docstring 总数:没有一个文件减少(不存在"翻译时顺手删了说明")
  单测:arm_application 233 / arm_driver_flexiv 69 / arm_interfaces 46 /
        arm_kinematics 24 / arm_sim_gazebo 236 = 608 passed,与改动前一致
  全部 .sh:bash -n 通过;并额外实际执行了 --help 验证(见下一个提交:
        bash -n 对 CRLF 脚本会漏报,只有真跑才暴露)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 17:40:55 +08:00

381 lines
14 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
# 手眼标定辅助脚本。
# 脚本:~/ros2/scripts/run_handeye.sh
# 配置与结果:~/ros2/handeye/ ← 见 handeye/README.md
# poses/<臂号>_<模式>.yaml 采点(输入)
# results/<臂号>_<模式>.yaml 标定结果(输出)
# calibrate.yaml 板尺寸 / 运动 / 稳定性参数
# tmp/ 临时产物
# 站点覆盖项:~/ros2/config/site.yaml → handeye:
#
# 用法:
# bash ~/ros2/scripts/run_handeye.sh calibrate
# bash ~/ros2/scripts/run_handeye.sh waypoints
# bash ~/ros2/scripts/run_handeye.sh teach # RDK 软拖拽(推荐)
# bash ~/ros2/scripts/run_handeye.sh teach --ros --auto-solve
# # 示教器模式 + 采完自动 merge 并直接标定(一条命令跑完)
# bash ~/ros2/scripts/run_handeye.sh teach --points 12 # 目标采 12 个点
# bash ~/ros2/scripts/run_handeye.sh teach --append # 接着上次点位继续采
# bash ~/ros2/scripts/run_handeye.sh teach --ros # 示教器 Free Drive
# bash ~/ros2/scripts/run_handeye.sh merge [taught.txt]
# bash ~/ros2/scripts/run_handeye.sh show # 看当前配置与点数
#
# 关于 --auto-solve(采完自动标定)的重要限制:
# 只有 --ros(示教器)模式能真正一条命令跑完。因为标定必须在 run_real.sh 的
# 控制器上走点位,而 --rdk 软拖拽要 RDK 独占,两者互斥 —— 这是硬件约束。
# --rdk --auto-solve 会照常自动 merge,然后明确告诉你接下来那两条命令。
#
# 全局选项(放在子命令前或后都可以):
# --arm 89|91|left|right 选臂(默认 89)
# --mode eye_in_hand|eye_to_hand
# 选模式。会切换到 <臂号>_<模式>.yaml,
# 所以 eye_in_hand 与 eye_to_hand 的结果互不覆盖
# --points N 仅 teach:目标采集点数(0=不限)
#
# 例:
# bash ~/ros2/scripts/run_handeye.sh --arm 91 teach --mode eye_to_hand --points 15
#
# calibrate / waypoints 需要:
# T1: bash ~/ros2/scripts/run_real.sh --arm 89|91
# T2: bash ~/ros2/scripts/run_camera_d405.sh --arm 89|91 --preview
# teach(拖拽)需要: 先停掉 run_real.sh 和相机(RDK + D405 都独占)
# 89 与 91 的结果文件、示教点、workdir 完全分开,禁止混用。
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/scripts/lib/parse_arm_args.sh"
parse_arm_args "$@"
set -- "${PARSE_ARM_REST[@]}"
CMD="${1:-}"
shift || true
usage() {
sed -n '2,40p' "$0"
}
# shellcheck disable=SC1091
source "${ROOT}/scripts/env.sh"
# shellcheck disable=SC1091
source "${ROOT}/scripts/lib/load_site_config.sh"
# 优先用包的源码树(editable 模式);退而用 install 里的 share
APP_CFG="${ROOT}/src/arm_application/config"
if [[ ! -d "${APP_CFG}" ]]; then
APP_CFG="$(ros2 pkg prefix arm_application 2>/dev/null)/share/arm_application/config" || true
fi
# 手眼标定相关文件现在集中在仓库根的 handeye/ 下(见 handeye/README.md)。
# 这些 SHE 值正常情况下由 site.yaml → export_site_env.py 提供;
# 下面只是 shell 侧的兜底,路径必须与 export_site_env.py 的默认值一致。
HE_DIR="${SITE_HANDEYE_DIR:-${ROOT}/handeye}"
WP="${SITE_HANDEYE_WAYPOINTS:-${HE_DIR}/poses/89_eye_in_hand.yaml}"
CAL="${SITE_HANDEYE_CALIBRATE:-${HE_DIR}/calibrate.yaml}"
TOUR="${SITE_HANDEYE_TOUR:-${HE_DIR}/waypoints.yaml}"
RES="${SITE_HANDEYE_FILE:-${HE_DIR}/results/89_eye_in_hand.yaml}"
WORKDIR="${SITE_HANDEYE_WORKDIR:-${HE_DIR}/tmp}"
TAUGHT="${WORKDIR}/taught_poses.txt"
PREFIX="${SITE_JOINT_PREFIX:-Rizon4-063989_}"
CAM="${SITE_CAM_NAME:-d405}"
# ---- --mode:切换 eye-in-hand / eye-to-hand ----
# 必须在 WP/RES 定下来之前处理,因为它会改变读写的文件名。
# 其余参数原样留给子命令(teach 自己有 --rdk/--ros 等)。
HANDEYE_MODE="${SITE_HANDEYE_MODE:-eye_in_hand}"
_rest=()
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
HANDEYE_MODE="${2:?--mode 需要 eye_in_hand 或 eye_to_hand}"
if [[ "${HANDEYE_MODE}" != "eye_in_hand" && "${HANDEYE_MODE}" != "eye_to_hand" ]]; then
echo "ERROR: --mode 只能是 eye_in_hand 或 eye_to_hand,收到 '${HANDEYE_MODE}'" >&2
exit 1
fi
shift 2
;;
*)
_rest+=("$1")
shift
;;
esac
done
set -- "${_rest[@]}"
# 臂号取 Flexiv 序列号后两位:Rizon4-063989 → 89
_arm_id="${SITE_ROBOT_SN: -2}"
if [[ -n "${_arm_id}" && "${_arm_id}" != "${SITE_ROBOT_SN}" ]]; then
if [[ "${HANDEYE_MODE}" != "${SITE_HANDEYE_MODE:-eye_in_hand}" ]]; then
# 显式指定了非默认模式 → 按 <臂号>_<模式>.yaml 重新定位
WP="${HE_DIR}/poses/${_arm_id}_${HANDEYE_MODE}.yaml"
RES="${HE_DIR}/results/${_arm_id}_${HANDEYE_MODE}.yaml"
echo "[handeye] --mode ${HANDEYE_MODE} → poses: ${WP}"
echo "[handeye] result: ${RES}"
fi
fi
_kill_servo() {
pkill -f 'visual_servo.launch' 2>/dev/null || true
pkill -f 'lib/arm_application/servo_node' 2>/dev/null || true
pkill -f 'lib/arm_application/perception_node' 2>/dev/null || true
sleep 1
}
_need_files() {
local f
for f in "$@"; do
if [[ ! -f "$f" ]]; then
echo "ERROR: missing $f" >&2
exit 2
fi
done
}
_need_robot() {
if ! ros2 service list 2>/dev/null | grep -q '/set_named_pose'; then
echo "ERROR: start run_real.sh first" >&2
exit 1
fi
}
_need_camera() {
if ! ros2 topic list 2>/dev/null | grep -q "/${CAM}/color/image_raw"; then
echo "ERROR: no /${CAM}/color/image_raw — start run_camera_d405.sh" >&2
exit 1
fi
}
cmd_show() {
echo "=== hand-eye (package + site) ==="
echo " arm : ${SITE_ROBOT_SN:-} ${SITE_ROBOT_LABEL:-}"
echo " package cfg : ${APP_CFG}"
echo " calibrate : ${CAL}"
echo " poses : ${WP}"
echo " tour : ${TOUR}"
echo " result : ${RES}"
echo " workdir : ${WORKDIR}"
echo " board : ${SITE_HANDEYE_BOARD_COLS:-?}x${SITE_HANDEYE_BOARD_ROWS:-?} inner, square=${SITE_HANDEYE_SQUARE_M:-?} m"
python3 "${ROOT}/scripts/lib/export_site_env.py" --print || true
if [[ -f "${WP}" ]]; then
python3 - <<PY
import re
from pathlib import Path
t = Path("${WP}").read_text(encoding="utf-8")
m = re.search(r"num_poses:\s*(\d+)", t)
n = int(m.group(1)) if m else "?"
print(f" num_poses in poses yaml: {n}")
PY
fi
}
cmd_merge() {
local src="${1:-${TAUGHT}}"
_need_files "${src}" "${WP}"
python3 "${ROOT}/scripts/merge_handeye_poses.py" "${src}" "${WP}"
echo "=== next: bash ~/ros2/scripts/run_handeye.sh calibrate ==="
}
cmd_calibrate() {
_need_files "${CAL}" "${WP}"
_need_robot
_need_camera
echo "=== kill visual_servo (avoid traj fight) ==="
_kill_servo
mkdir -p "${WORKDIR}"
local cols="${SITE_HANDEYE_BOARD_COLS:-8}"
local rows="${SITE_HANDEYE_BOARD_ROWS:-11}"
local sq="${SITE_HANDEYE_SQUARE_M:-0.015}"
local vel="${SITE_HANDEYE_VEL_SCALE:-0.05}"
local mt="${SITE_HANDEYE_MOVE_T:-8.0}"
local st="${SITE_HANDEYE_SETTLE_SEC:-1.5}"
local qd="${SITE_HANDEYE_SETTLE_QD:-0.015}"
local ss="${SITE_HANDEYE_SETTLE_STABLE:-0.6}"
local sto="${SITE_HANDEYE_SETTLE_TO:-8.0}"
local ph="${SITE_HANDEYE_PIN_HOLDS:-2}"
local ps="${SITE_HANDEYE_PIN_SEC:-0.45}"
local skip="${SITE_HANDEYE_SKIP_UNSTABLE:-true}"
echo "=== calibrate mode=${HANDEYE_MODE} ==="
echo " board ${cols}x${rows} inner, square=${sq} m (from site.yaml)"
echo " motion v=${vel} T≥${mt}s settle=${st}s qd≤${qd} pin=${ph}x${ps}s"
echo " poses=${WP}"
echo " result → ${RES}"
exec ros2 run arm_application handeye_calibrate --ros-args \
--params-file "${CAL}" \
--params-file "${WP}" \
-p joint_prefix:="${PREFIX}" \
-p camera_source:="${CAM}" \
-p output_dir:="${WORKDIR}" \
-p result_file:="${RES}" \
-p mode:="${HANDEYE_MODE}" \
-p board_cols:="${cols}" \
-p board_rows:="${rows}" \
-p square_size_m:="${sq}" \
-p velocity_scale:="${vel}" \
-p move_time_sec:="${mt}" \
-p settle_sec:="${st}" \
-p settle_qd_max:="${qd}" \
-p settle_stable_sec:="${ss}" \
-p settle_timeout_sec:="${sto}" \
-p pin_holds:="${ph}" \
-p pin_hold_sec:="${ps}" \
-p skip_unstable_poses:="${skip}"
}
cmd_waypoints() {
_need_files "${TOUR}" "${WP}"
_need_robot
_need_camera
echo "=== kill visual_servo ==="
_kill_servo
mkdir -p "${WORKDIR}"
echo "=== waypoint tour (Enter=confirm, q=quit) ==="
echo " saves confirmed joints → ${TAUGHT}"
echo " then: bash ~/ros2/scripts/run_handeye.sh merge"
exec ros2 run arm_application handeye_waypoints --ros-args \
--params-file "${TOUR}" \
--params-file "${WP}" \
-p joint_prefix:="${PREFIX}" \
-p camera_source:="${CAM}" \
-p append_poses_file:="${TAUGHT}"
}
cmd_teach() {
local backend="rdk"
local append=false
local points=0
local auto_solve=false
while [[ $# -gt 0 ]]; do
case "$1" in
--rdk) backend="rdk"; shift ;;
--ros|--pendant) backend="ros"; shift ;;
--append|--keep) append=true; shift ;;
--points) points="${2:?--points 需要一个数字}"; shift 2 ;;
--auto-solve) auto_solve=true; shift ;;
*) echo "Unknown teach arg: $1" >&2; exit 1 ;;
esac
done
echo "=== kill visual_servo + camera (D405 exclusive) ==="
_kill_servo
pkill -f 'run_camera_d405' 2>/dev/null || true
pkill -f 'realsense_color_pub' 2>/dev/null || true
pkill -f 'realsense2_camera_node' 2>/dev/null || true
sleep 1
if [[ "${backend}" == "rdk" ]]; then
if ros2 service list 2>/dev/null | grep -q '/set_named_pose'; then
echo "ERROR: RDK 拖拽与 run_real.sh 互斥。请先在那个终端 Ctrl-C 停掉 run_real.sh" >&2
exit 1
fi
else
_need_robot
fi
mkdir -p "${WORKDIR}/images"
if [[ "${append}" == true ]]; then
echo "=== append: keep existing ${TAUGHT} ==="
else
# 重新设点:丢掉上次 taught,避免 merge 混进旧位姿
: > "${TAUGHT}"
fi
echo "=== interactive teach backend=${backend} → ${WORKDIR} ==="
echo " 窗口按键: d=拖拽开 s=拖拽关 Enter=拍照记点 q=结束"
if [[ "${points}" -gt 0 ]] 2>/dev/null; then
echo " 目标 ${points} 个点(采够会有提示,仍可继续多采)"
else
echo " 建议 8–12 个姿态,棋盘占画面约 30–70%"
echo " (也可用 --points N 设定目标点数)"
fi
echo " 结束后: bash ~/ros2/scripts/run_handeye.sh merge"
echo " bash ~/ros2/scripts/run_handeye.sh calibrate"
if [[ "${auto_solve}" == true ]]; then
echo " --auto-solve: 采完会自动 merge,并视后端决定是否直接标定"
fi
TEACH_ARGS=(
-p backend:="${backend}"
-p robot_sn:="${SITE_ROBOT_SN:-Rizon4-063989}"
-p joint_prefix:="${PREFIX}"
-p camera_source:="${CAM}"
-p output_dir:="${WORKDIR}"
-p target_points:="${points}"
)
if [[ -n "${SITE_CAM_SERIAL:-}" ]]; then
TEACH_ARGS+=(-p camera_serial:="${SITE_CAM_SERIAL}")
fi
if [[ -n "${SITE_CAM_WIDTH:-}" ]]; then
TEACH_ARGS+=(-p width:="${SITE_CAM_WIDTH}" -p height:="${SITE_CAM_HEIGHT:-480}" -p fps:="${SITE_CAM_FPS:-30}")
fi
TEACH_ARGS+=(
-p auto_exposure:="$([ "${SITE_CAM_AUTO_EXPOSURE:-1}" = "1" ] && echo true || echo false)"
-p ae_limit_us:="${SITE_CAM_AE_LIMIT_US:-1800}"
-p emitter_enabled:="$([ "${SITE_CAM_EMITTER:-0}" = "1" ] && echo true || echo false)"
-p exposure_us:="${SITE_CAM_EXPOSURE_US:-0}"
-p gain:="${SITE_CAM_GAIN:-0}"
)
ros2 run arm_application handeye_teach --ros-args "${TEACH_ARGS[@]}" || true
# ---- 采点结束 ----
if [[ "${auto_solve}" != true ]]; then
echo
echo "采点结束。接着跑(或下次加 --auto-solve 让它自动接):"
echo " bash ~/ros2/scripts/run_handeye.sh merge --arm ${SITE_ROBOT_ARM:-89}"
echo " bash ~/ros2/scripts/run_handeye.sh calibrate --arm ${SITE_ROBOT_ARM:-89}"
return 0
fi
echo
echo "=== --auto-solve: 自动 merge ==="
if [[ ! -s "${TAUGHT}" ]]; then
echo "ERROR: 没采到任何点(${TAUGHT} 为空),中止。" >&2
return 1
fi
if ! python3 "${ROOT}/scripts/merge_handeye_poses.py" "${TAUGHT}" "${WP}"; then
echo "ERROR: merge 失败,点还在 ${TAUGHT},可手动重跑 merge。" >&2
return 1
fi
echo " 已写入 ${WP}"
# RDK 软拖拽与控制器互斥:软拖拽要 RDK 独占,标定必须在 run_real.sh 的
# 控制器上走点位,两者无法同时存在。这是硬件约束,脚本绕不过去,
# 所以这里如实停下并给出下一步,而不是硬跑一个必然失败的 calibrate。
if [[ "${backend}" == "rdk" ]]; then
echo
echo "⚠️ 当前是 RDK 拖拽模式(--rdk),无法自动接着求解。"
echo " 原因:软拖拽需要 RDK 独占,而标定需要通过 run_real.sh 的控制器走点位,"
echo " 两者不能同时存在。"
echo
echo " 继续两步即可:"
echo " 1) 另开终端: bash ~/ros2/scripts/run_real.sh --arm ${SITE_ROBOT_ARM:-89}"
echo " 2) 回到这里: bash ~/ros2/scripts/run_handeye.sh calibrate --arm ${SITE_ROBOT_ARM:-89}"
echo
echo " 想真正一条命令跑完,请改用示教器 Free Drive 模式:"
echo " bash ~/ros2/scripts/run_handeye.sh teach --ros --auto-solve"
return 0
fi
# --ros(示教器)后端:控制器本来就在跑,可以无缝接上标定。
# 但 teach 阶段把相机杀掉了(USB 独占),标定拍照要用,得先拉回来。
echo
echo "=== 重启相机(teach 阶段杀掉了它,标定拍照需要)==="
nohup bash "${ROOT}/scripts/run_camera_d405.sh" --arm "${SITE_ROBOT_ARM:-89}" \
--no-preview > /tmp/run_handeye_camera.log 2>&1 &
echo " pid=$! log=/tmp/run_handeye_camera.log"
# _need_camera 内部失败时会自己打印原因并 exit,不需要在这里再判一次。
_need_camera
echo
echo "=== --auto-solve: 自动 calibrate ==="
cmd_calibrate
}
case "${CMD}" in
calibrate|calib) cmd_calibrate ;;
waypoints|tour) cmd_waypoints "$@" ;;
teach) cmd_teach "$@" ;;
merge) cmd_merge "$@" ;;
show) cmd_show ;;
-h|--help|help|"") usage; [[ -n "${CMD}" ]] || exit 0; exit 0 ;;
*) echo "Unknown: ${CMD}" >&2; usage >&2; exit 1 ;;
esac