Files
ros_contorl/scripts/go_home.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

284 lines
8.5 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
# 把 Rizon4 移动到指定命名的位姿(默认:home)并强制停止。
#
# 关键:必须先杀掉 visual_servo.launch / servo_node,否则
# 它们会一直发 JointTrajectory,机械臂永远稳定不下来。
#
# 用法:
# bash ~/ros2/scripts/go_home.sh
# bash ~/ros2/scripts/go_home.sh --pose ready --speed 0.3
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
POSE="${POSE:-}"
SPEED="${SPEED:-}"
KEEP_SERVO=0
# shellcheck disable=SC1091
source "${ROOT}/scripts/lib/parse_arm_args.sh"
parse_arm_args "$@"
set -- "${PARSE_ARM_REST[@]}"
usage() {
cat <<'EOF'
Move arm to a named pose (default: home), then hold with zero velocity.
Default: kill perception/servo/launch first (required to stop shaking).
Options:
--pose NAME home | ready | inspect | pos_left
--speed FLOAT velocity_scale (0,1] default 0.25
--arm 89|91 与 run_real 同一臂
--keep-servo do NOT kill servo (arm may keep shaking)
-h, --help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--pose) POSE="${2:?}"; shift 2 ;;
--speed) SPEED="${2:?}"; shift 2 ;;
--keep-servo) KEEP_SERVO=1; shift ;;
--stop-track) shift ;; # 历史遗留
-h|--help) usage; exit 0 ;;
*) echo "Unknown: $1" >&2; usage >&2; exit 1 ;;
esac
done
if ! command -v ros2 >/dev/null 2>&1 || [[ -z "${ROS_DISTRO:-}" ]]; then
# shellcheck disable=SC1091
source "${ROOT}/scripts/env.sh"
fi
# shellcheck disable=SC1091
source "${ROOT}/scripts/lib/load_site_config.sh"
POSE="${POSE:-${SITE_GO_HOME_POSE:-home}}"
SPEED="${SPEED:-${SITE_GO_HOME_SPEED:-0.25}}"
kill_streamers() {
echo "=== killing visual-servo streamers ==="
# 能先禁用控制就禁用(如果节点还活着),再动手 kill
ros2 param set /servo_node enable_control false >/dev/null 2>&1 || true
# 先杀 launch 父进程,再杀可执行文件(先 SIGTERM 再 SIGKILL)
local patterns=(
'visual_servo.launch.py'
'/lib/arm_application/servo_node'
'/lib/arm_application/perception_node'
)
local p
for p in "${patterns[@]}"; do
if pgrep -f "$p" >/dev/null 2>&1; then
echo " TERM $p"
pkill -TERM -f "$p" 2>/dev/null || true
fi
done
sleep 1.0
for p in "${patterns[@]}"; do
if pgrep -f "$p" >/dev/null 2>&1; then
echo " KILL $p"
pkill -KILL -f "$p" 2>/dev/null || true
fi
done
sleep 0.5
if pgrep -f '/lib/arm_application/servo_node' >/dev/null 2>&1; then
echo "ERROR: servo_node still alive — refuse to move (would keep shaking)" >&2
pgrep -af '/lib/arm_application/servo_node' >&2 || true
exit 2
fi
echo " servo_node gone OK"
}
print_joints() {
python3 <<'PY'
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import JointState
rclpy.init()
n = Node("go_home_joints")
box = [None]
n.create_subscription(JointState, "joint_states", lambda m: box.__setitem__(0, m), 10)
for _ in range(40):
rclpy.spin_once(n, timeout_sec=0.05)
if box[0] is not None:
break
if box[0] is None:
print(" (no joint_states)")
else:
m = dict(zip(box[0].name, box[0].position))
v = dict(zip(box[0].name, box[0].velocity)) if box[0].velocity else {}
for i in range(1, 8):
k = f"{__import__('os').environ.get('SITE_JOINT_PREFIX','Rizon4-063989_')}joint{i}"
if k in m:
print(f" joint{i}: {m[k]:+.4f} qd={v.get(k, 0.0):+.4f}")
n.destroy_node(); rclpy.shutdown()
PY
}
# 用控制器的 ACTION 做强力稳定(不走话题 —— 这样能可靠地匹配 JTC)
settle_hold_action() {
local pose="$1"
python3 - "$pose" <<'PY'
import sys, time
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from control_msgs.action import FollowJointTrajectory
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
from sensor_msgs.msg import JointState
POSES = {
"home": [0.0, -0.698, 0.0, 1.57, 0.0, 0.698, 0.0],
"ready": [0.0, -0.698, 0.0, 1.57, 0.0, 0.698, 0.0],
"inspect": [0.698, -0.698, 0.698, 1.57, 0.698, 0.698, 0.698],
"pos_left": [-0.698, -0.698, 0.698, 1.57, 0.698, 0.698, 0.698],
}
name = sys.argv[1]
target = POSES.get(name)
if target is None:
raise SystemExit(f"unknown pose {name}")
import os
PREFIX = os.environ.get("SITE_JOINT_PREFIX", "Rizon4-063989_")
JOINTS = [f"{PREFIX}joint{i}" for i in range(1, 8)]
rclpy.init()
n = Node("go_home_hold_action")
js_box = [None]
n.create_subscription(JointState, "joint_states", lambda m: js_box.__setitem__(0, m), 10)
cli = ActionClient(n, FollowJointTrajectory, "rizon_arm_controller/follow_joint_trajectory")
if not cli.wait_for_server(timeout_sec=5.0):
print(" WARN: JTC action not available — skip action hold")
n.destroy_node(); rclpy.shutdown(); raise SystemExit(0)
def spin_until(pred, timeout=3.0):
t0 = time.monotonic()
while time.monotonic() - t0 < timeout:
rclpy.spin_once(n, timeout_sec=0.05)
if pred():
return True
return False
spin_until(lambda: js_box[0] is not None, 3.0)
# 两点轨迹:当前位姿 -> 目标位姿(或保持在目标位姿),两点速度都为零
q_now = list(target)
if js_box[0] is not None:
m = dict(zip(js_box[0].name, js_box[0].position))
try:
q_now = [float(m[j]) for j in JOINTS]
except KeyError:
pass
def send_hold(q, T=0.40):
traj = JointTrajectory()
traj.joint_names = list(JOINTS)
# 起点
p0 = JointTrajectoryPoint()
p0.positions = [float(v) for v in q]
p0.velocities = [0.0] * 7
p0.accelerations = [0.0] * 7
p0.time_from_start.sec = 0
p0.time_from_start.nanosec = 50_000_000
# 终点是同一姿势,速度仍为零(钉住)
p1 = JointTrajectoryPoint()
p1.positions = [float(v) for v in q]
p1.velocities = [0.0] * 7
p1.accelerations = [0.0] * 7
ns = int(round(T * 1e9))
p1.time_from_start.sec = ns // 1_000_000_000
p1.time_from_start.nanosec = ns % 1_000_000_000
traj.points = [p0, p1]
goal = FollowJointTrajectory.Goal()
goal.trajectory = traj
fut = cli.send_goal_async(goal)
spin_until(lambda: fut.done(), 3.0)
if not fut.done():
print(" hold goal send timeout")
return False
gh = fut.result()
if gh is None or not gh.accepted:
print(" hold goal rejected")
return False
rfut = gh.get_result_async()
spin_until(lambda: rfut.done(), T + 3.0)
return True
# 1) 先朝命名位姿快速靠拢,末端速度为零
print(f" action hold → {name} (zero vel)")
send_hold(target, T=0.60)
time.sleep(0.4)
# 2–4) 反复把实测位姿重新钉住(让 JTC 的开环指令与编码器读数对齐)
for i in range(3):
spin_until(lambda: js_box[0] is not None, 1.0)
if js_box[0] is None:
break
m = dict(zip(js_box[0].name, js_box[0].position))
q_pin = [float(m[j]) for j in JOINTS]
print(f" action re-pin #{i+1} @ measured joints (zero vel)")
send_hold(q_pin, T=0.45)
time.sleep(0.25)
# 等到 |qd| 足够小
print(" waiting for joints to stop ...")
t0 = time.monotonic()
while time.monotonic() - t0 < 5.0:
rclpy.spin_once(n, timeout_sec=0.05)
if js_box[0] is None or not js_box[0].velocity:
continue
vmap = dict(zip(js_box[0].name, js_box[0].velocity))
vmax = max(abs(float(vmap.get(j, 0.0))) for j in JOINTS)
if vmax < 0.015:
print(f" settled |qd|_max={vmax:.4f} rad/s")
break
else:
print(" WARN: still some residual velocity")
n.destroy_node()
rclpy.shutdown()
PY
}
if [[ "${KEEP_SERVO}" -eq 0 ]]; then
kill_streamers
else
echo "WARNING: --keep-servo — shaking likely"
fi
if ! ros2 service list 2>/dev/null | grep -q '/set_named_pose'; then
echo "ERROR: /set_named_pose missing — start run_real.sh" >&2
exit 1
fi
echo "=== joints before ==="
print_joints
echo "=== move: ${POSE} speed=${SPEED} ==="
RESP="$(
ros2 service call /set_named_pose arm_msgs/srv/SetNamedPose \
"{pose_name: '${POSE}', velocity_scale: ${SPEED}, acceleration_scale: 0.0}" 2>&1
)"
echo "${RESP}"
if ! echo "${RESP}" | grep -q 'success=True'; then
echo "ERROR: move failed" >&2
exit 1
fi
WAIT_SEC="$(python3 -c "print(max(2.5, 1.0 / max(float('${SPEED}'), 0.05) + 2.0))")"
echo "Waiting ${WAIT_SEC}s ..."
sleep "${WAIT_SEC}"
echo "=== settle (action, vel=0) ==="
settle_hold_action "${POSE}"
# 最后确认:servo 必须仍然是死的
if pgrep -f '/lib/arm_application/servo_node' >/dev/null 2>&1; then
echo "WARNING: servo_node came back — killing again"
kill_streamers
settle_hold_action "${POSE}"
fi
echo "=== joints after ==="
print_joints
echo "Done. Arm should be still."
echo "Tracking later: bash ~/ros2/scripts/run_visual_servo_rt.sh --control"