Files
ros_contorl/scripts/run_handeye.sh
arm_dev b903161113 左臂换成 tool10120938 后,手眼仍按旧 tool1 和 8×11 板求解,顶视迁臂又按短跳 rest-to-rest,真机上要么整体偏一截,要么中途弹射启停。
眼在手上改用该 TCP 做 AX=XB,再折回相机→法兰;A5 内角点改为 11×8,09-04 / 10-09 旧结果留在 backup。一阶段 D435 迁臂改成一条 FollowJointTrajectory(余弦加减速,近处降速,XY 进门槛后再单独收垂直),跟踪锁高与悬停齐平,避免倾斜相机下先降一截把孔位重投影偏掉。--no-descent 只降到离面 15 mm;跳过三阶段时触觉 USB 打不开不再空等几分钟。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 15:04:23 +08:00

478 lines
19 KiB
Bash
Executable File
Raw Permalink 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)。
# 这些值全部由 site.yaml → export_site_env.py 提供,**包括文件名里的臂号与相机名**
# (臂号 = 该臂 SN 末两位,由 arm_short_id() 现算;相机名来自 cameras 注册表)。
#
# 这里刻意**不**自己拼 `<臂号>_<模式>.yaml`:`_arm_id`、`HANDEYE_MODE` 与相机名
# 在下面几行之后才算得出来,在这里拼会拼出 `results/_.yaml` 这种空文件名,
# 而症状是"标定写完了、用的时候读不到"。
HE_DIR="${SITE_HANDEYE_DIR:-${ROOT}/handeye}"
CAL="${SITE_HANDEYE_CALIBRATE:-${HE_DIR}/calibrate.yaml}"
TOUR="${SITE_HANDEYE_TOUR:-${HE_DIR}/waypoints.yaml}"
WORKDIR="${SITE_HANDEYE_WORKDIR:-${HE_DIR}/tmp}"
PREFIX="${SITE_JOINT_PREFIX:?SITE_JOINT_PREFIX 为空 —— site.yaml 里需要该臂的 robot.sn}"
# ---- --mode:切换 eye-in-hand / eye-to-hand ----
# 必须在 WP/RES/板/相机定下来之前处理:模式决定了读哪个结果文件、用哪块板、
# 开哪台相机、采点落到哪个文件。其余参数原样留给子命令(teach 自己有 --rdk 等)。
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}"
# 取"当前模式"的那一份值。名字由 export_site_env 按模式导出成
# SITE_HANDEYE_<字段>_EYE_IN_HAND / _EYE_TO_HAND
# 这样"模式 → 结果文件 / 示教点 / 标定板"的对应关系只有 site.yaml 一处定义,
# 脚本不再自己拼文件名(拼错/漏拼都是**静默**用错文件,最难查)。
_HE_TAG="$(echo "${HANDEYE_MODE}" | tr '[:lower:]' '[:upper:]')"
_he() {
local name="SITE_HANDEYE_$1_${_HE_TAG}"
printf '%s' "${!name-}"
}
WP="$(_he WAYPOINTS)"
WP="${WP:-${HE_DIR}/poses/${_arm_id:-?}_${HANDEYE_MODE}.yaml}"
RES="$(_he FILE)"
RES="${RES:-${HE_DIR}/results/${_arm_id:-?}_${HANDEYE_MODE}.yaml}"
# 示教点临时文件也按模式分开:两个模式共用一份时,后一个 teach 会清掉前一个的采点,
# 而症状只是"merge 出来的点位不对",很难联想到是文件串了。
TAUGHT="${WORKDIR}/taught_poses_${HANDEYE_MODE}.txt"
# ---- 相机:两种模式用的是**两台不同的相机** ----
# 眼在手上 = 腕部相机(跟着臂动);眼在手外 = 固定在工位外的相机(不随臂动)。
# 原来这里恒用 SITE_CAM_NAME(腕部),于是 `--mode eye_to_hand` 会去开腕部相机 ——
# 板跟着臂动、相机也跟着臂动,"标定"能跑完,但解出的是个毫无意义的错外参。
if [[ "${HANDEYE_MODE}" == "eye_to_hand" ]]; then
CAM="${SITE_OVERHEAD_CAM_NAME:-}"
if [[ -z "${CAM}" ]]; then
echo "ERROR: eye_to_hand 需要一台固定相机,但 site.yaml 的 cameras 段里没有登记的。" >&2
echo " 加一条 role: overhead_eye_to_hand(眼在手外、不随臂动)的相机即可。" >&2
exit 2
fi
CAM_SERIAL="${SITE_OVERHEAD_CAM_SERIAL:-}"
CAM_WIDTH="${SITE_OVERHEAD_CAM_WIDTH:-}"
CAM_HEIGHT="${SITE_OVERHEAD_CAM_HEIGHT:-}"
CAM_FPS="${SITE_OVERHEAD_CAM_FPS:-30}"
CAM_AE="${SITE_OVERHEAD_CAM_AUTO_EXPOSURE:-1}"
CAM_AE_LIMIT="${SITE_OVERHEAD_CAM_AE_LIMIT_US:-8000}"
CAM_EMITTER="${SITE_OVERHEAD_CAM_EMITTER:-1}"
CAM_EXPOSURE="${SITE_OVERHEAD_CAM_EXPOSURE_US:-0}"
CAM_GAIN="${SITE_OVERHEAD_CAM_GAIN:-0}"
else
CAM="${SITE_CAM_NAME:-d405}"
CAM_SERIAL="${SITE_CAM_SERIAL:-}"
CAM_WIDTH="${SITE_CAM_WIDTH:-}"
CAM_HEIGHT="${SITE_CAM_HEIGHT:-}"
CAM_FPS="${SITE_CAM_FPS:-30}"
CAM_AE="${SITE_CAM_AUTO_EXPOSURE:-1}"
CAM_AE_LIMIT="${SITE_CAM_AE_LIMIT_US:-1800}"
CAM_EMITTER="${SITE_CAM_EMITTER:-0}"
CAM_EXPOSURE="${SITE_CAM_EXPOSURE_US:-0}"
CAM_GAIN="${SITE_CAM_GAIN:-0}"
fi
echo "[handeye] mode=${HANDEYE_MODE} poses=${WP}"
echo "[handeye] result=${RES}"
echo "[handeye] camera=${CAM} serial=${CAM_SERIAL:-(空)}"
_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 " camera : ${CAM} serial=${CAM_SERIAL:-(空)}"
echo " tool : ${SITE_ROBOT_TOOL_NAME:-(不切换)} gripper_tool=${SITE_HANDEYE_GRIPPER_TOOL:-(无,ee_pose 原样)}"
echo " board : $(_he BOARD_COLS)x$(_he BOARD_ROWS) inner, square=$(_he SQUARE) m (来源=$(_he BOARD_SRC))"
echo " (另一种模式的板见下面 export 的 标定板[...] 两行)"
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() {
# 先查配置(板尺寸)再看硬件:配置没配好的话,连硬件都不该去动。
local cols="$(_he BOARD_COLS)"
local rows="$(_he BOARD_ROWS)"
local sq="$(_he SQUARE)"
local src="$(_he BOARD_SRC)"
# 板尺寸必须**真的配过**。没配就开标定 = 赌一个方格边长,而错了不会报错,
# 只会让结果系统性偏(见 handeye/README.md 第 3 节)。所以这里直接拦下。
if [[ -z "${src}" || "${src}" == "default" ]]; then
echo "ERROR: 没有配置 [${HANDEYE_MODE}] 用的标定板尺寸。" >&2
echo " 在 config/site.yaml 的 handeye.boards.${HANDEYE_MODE} 里写:" >&2
echo " cols / rows / square_size_m" >&2
echo " (内角点 = 方格数 − 1;测一下实际方格边长,别照抄另一块板。)" >&2
exit 2
fi
_need_files "${CAL}" "${WP}"
local grip_tool="${SITE_HANDEYE_GRIPPER_TOOL:-}"
local grip_xyz="${SITE_HANDEYE_GRIPPER_TCP_XYZ:-}"
local grip_q="${SITE_HANDEYE_GRIPPER_TCP_QWXYZ:-}"
if [[ -n "${grip_tool}" ]]; then
if [[ "${SITE_ROBOT_TOOL_NAME:-}" != "${grip_tool}" ]]; then
echo "ERROR: 手眼要按工具 ${grip_tool} 求解,但 site 当前激活工具是 '${SITE_ROBOT_TOOL_NAME:-}'。" >&2
echo " robots[].tool_name 必须和 handeye.gripper_tool 同一份名字。" >&2
exit 2
fi
if [[ -z "${grip_xyz}" || -z "${grip_q}" ]]; then
echo "ERROR: 配了 gripper_tool=${grip_tool} 但没写 gripper_tcp_xyz / gripper_tcp_qwxyz。" >&2
exit 2
fi
fi
_need_robot
_need_camera
echo "=== kill visual_servo (avoid traj fight) ==="
_kill_servo
mkdir -p "${WORKDIR}"
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 (site.yaml handeye.boards.${HANDEYE_MODE},来源=${src})"
echo " camera ${CAM} serial=${CAM_SERIAL:-(空)}"
echo " motion v=${vel} T≥${mt}s settle=${st}s qd≤${qd} pin=${ph}x${ps}s"
echo " poses=${WP}"
echo " result → ${RES}"
local grip_args=()
if [[ -n "${grip_tool}" ]]; then
if [[ "${HANDEYE_MODE}" == "eye_in_hand" ]]; then
echo " gripper_tool=${grip_tool} tcp_xyz=[${grip_xyz}] (AX=XB 用 TCP,T_4x4 折成 cam→法兰)"
else
echo " gripper_tool=${grip_tool} tcp_xyz=[${grip_xyz}] (AX=XB 用 TCP,T_4x4 仍是 cam→基座,不折)"
fi
grip_args+=(
-p gripper_tool:="${grip_tool}"
-p gripper_tcp_xyz:="[${grip_xyz}]"
-p gripper_tcp_qwxyz:="[${grip_q}]"
)
fi
exec ros2 run arm_application handeye_calibrate --ros-args \
--params-file "${CAL}" \
--params-file "${WP}" \
-p joint_prefix:="${PREFIX}" \
-p camera_source:="${CAM}" \
${CAM_SERIAL:+-p camera_serial:="${CAM_SERIAL}"} \
-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}" \
"${grip_args[@]}"
}
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:?手眼标定必须知道是哪台臂}"
-p joint_prefix:="${PREFIX}"
-p camera_source:="${CAM}"
-p output_dir:="${WORKDIR}"
-p poses_file:="${TAUGHT}"
-p target_points:="${points}"
)
if [[ -n "${CAM_SERIAL}" ]]; then
TEACH_ARGS+=(-p camera_serial:="${CAM_SERIAL}")
fi
if [[ -n "${CAM_WIDTH}" ]]; then
TEACH_ARGS+=(-p width:="${CAM_WIDTH}" -p height:="${CAM_HEIGHT:-480}" -p fps:="${CAM_FPS:-30}")
fi
TEACH_ARGS+=(
-p auto_exposure:="$([ "${CAM_AE:-1}" = "1" ] && echo true || echo false)"
-p ae_limit_us:="${CAM_AE_LIMIT:-1800}"
-p emitter_enabled:="$([ "${CAM_EMITTER:-0}" = "1" ] && echo true || echo false)"
-p exposure_us:="${CAM_EXPOSURE:-0}"
-p gain:="${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 阶段杀掉了它,标定拍照需要)==="
if [[ "${HANDEYE_MODE}" == "eye_to_hand" ]]; then
# 眼在手外用的是固定相机(D435),不是腕部 D405 —— 拉错相机的话,
# 标定会去订一个没人发布的图,表现是"相机没数据"。
_cam_cmd=(bash "${ROOT}/scripts/run_camera_d435.sh" --no-preview)
else
_cam_cmd=(bash "${ROOT}/scripts/run_camera_d405.sh" --arm "${SITE_ROBOT_ARM:-89}" --no-preview)
fi
nohup "${_cam_cmd[@]}" > /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