注释与提示信息全面中文化:源码 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>
This commit is contained in:
arm_dev
2026-09-23 17:40:55 +08:00
parent d78e66ce98
commit 72353f3773
115 changed files with 1758 additions and 1573 deletions
+9
View File
@@ -27,6 +27,15 @@
- 注意 `run_sim.sh` 不是 Gazebo,它走 mock 驱动(假数据 + RViz),永远不会产生接触力
**环境:** `env.sh` · `build_all.sh`
**RDK:** `test_rdk_python.sh` · `rdk_switch_tool.sh` · `rdk_drag_bridge.py`
**改注释/写文档后自检:** `python3 scripts/check_text_only.py`
- 用途:证明这次改动**只动了文字**(注释、docstring、提示信息),没碰逻辑。
它把改动前后的源码解析成语法树,删掉 docstring、抹平字符串常量后比对;
"翻译 f-string 时漏掉一个 `{}` 插值""顺手改了个变量名"这类问题会被抓出来。
- `python3 scripts/check_text_only.py` 查所有改过的 `.py`;也可以直接跟文件名。
基准是 `git HEAD`,所以要在**提交之前**跑。老手滑手滑的代价比跑一条命令高得多。
- 规矩写在 [docs/FRAMEWORK_RULES.md](../docs/FRAMEWORK_RULES.md) 的 R6.5。
一句话:**注释/docstring/提示信息用中文;标识符、话题名、字段名保留英文**
(但 `src/flexiv_*/` 是厂商上游代码,不许翻译,见 R4.4)。
**搁置未用:** `fetch_isaac_flexiv.sh`(拉 Flexiv 的 Isaac Sim / RDK 源码到 `thirdparty/`;Isaac 路线目前搁置,Gazebo 是选定平台)
`lib/parse_arm_args.sh` 解析 `--arm 89|91`。
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Build entire workspace: arm stack + flexiv real drivers
# 构建整个工作区:机械臂栈 + Flexiv 真机驱动
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Build Flexiv RDK v1.x into ~/flexiv_install (required for real robot / flexiv_hardware).
# 把 Flexiv RDK v1.x 构建安装到 ~/flexiv_install(真机 / `flexiv_hardware` 必需)。
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RDK="${ROOT}/src/flexiv_rdk"
@@ -11,7 +11,7 @@ if [[ ! -d "$RDK" ]]; then
fi
echo "Building RDK thirdparty deps into $PREFIX ..."
# Prefer system /opt/ros if present; else conda env already has compilers
# 有系统 /opt/ros 就优先用它;否则 conda 环境里已经有编译器
if [[ -f /opt/ros/humble/setup.bash ]]; then
# shellcheck disable=SC1091
source /opt/ros/humble/setup.bash
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Build Flexiv hardware packages for real robot (after RDK installed to ~/flexiv_install)
# 构建真机用的 Flexiv 硬件包(前提:RDK 已装到 ~/flexiv_install)
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Check PC ↔ Flexiv robot network (addresses from site.yaml)
# 检查 PC ↔ Flexiv 机器人网络(地址取自 site.yaml)
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""纯文本改动的安全闸门:证明一次改动**只动了文字**,没碰逻辑。
=============================================================================
为什么需要这个工具
=============================================================================
"把英文注释翻成中文""补一批说明文档"这类改动,看起来零风险,其实经常出事:
改注释的人手滑动了变量名、顺手调了语句顺序、翻译 f-string 时漏掉一个 ``{}`` 插值。
这些都不会报错,只在某天变成一句"结果怎么不对了"。
人工 review 靠不住(几十个文件、几千行 diff,没人真的逐行看)。
所以这里用 AST 做机器判据:**把两份源码解析成语法树,抹掉所有文字,再比对。**
抹掉规则:
1. 删掉所有 docstring
2. 所有字符串常量换成同一个占位符
3. f-string 只保留"插值了几次、插值的是什么表达式",
字面量部分整段丢掉、插值按内容排序
第 3 条是为中文语序专门留的口子。英文 ``f"on {port} read {idx} failed"``
翻成 ``f"在 {port} 上读 {idx} 失败"`` 只是把两个插值的**显示顺序**换了,
语义没变,不该判失败。但只要有人**多插一个、少插一个、或换掉被插入的表达式**,
这里立刻报出来。
=============================================================================
用法
=============================================================================
python3 scripts/check_text_only.py # 检查所有改过的 .py
python3 scripts/check_text_only.py <文件> [...] # 只检查指定文件
python3 scripts/check_text_only.py --staged # 只检查已 git add 的
比较基准固定为 ``git HEAD``(也就是"上一次提交"),所以要用在**提交之前**。
退出码 0 = 改动范围内没有逻辑变化;非 0 = 有文件动了不该动的东西(会打印差异)。
=============================================================================
"""
from __future__ import annotations
import argparse
import ast
import difflib
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
# 被抹平后的字符串统一长这样。用控制字符是为了保证它不会和真实内容撞车。
PLACEHOLDER = "\0STR\0"
class _Normalize(ast.NodeTransformer):
"""把语法树里的"文字"全部抹掉,只留下结构与标识符。"""
def _strip_docstring(self, node):
"""删掉模块 / 类 / 函数的第一个字符串表达式(docstring)。"""
self.generic_visit(node)
body = getattr(node, "body", None)
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) \
and isinstance(body[0].value.value, str):
node.body = body[1:]
return node
visit_Module = _strip_docstring
visit_FunctionDef = _strip_docstring
visit_AsyncFunctionDef = _strip_docstring
visit_ClassDef = _strip_docstring
def visit_Constant(self, node: ast.Constant):
if isinstance(node.value, str):
return ast.copy_location(ast.Constant(value=PLACEHOLDER), node)
return node
def visit_JoinedStr(self, node: ast.JoinedStr):
"""f-string:只留插值,不留字面量;插值按内容排序(见模块 docstring)。"""
self.generic_visit(node)
parts = [ast.dump(v) for v in node.values if isinstance(v, ast.FormattedValue)]
return ast.copy_location(
ast.Constant(value="FSTR[" + "|".join(sorted(parts)) + "]"), node)
def normalize(source: str) -> str:
"""源码 -> 抹掉文字后的规范化结构字符串。"""
return ast.dump(_Normalize().visit(ast.parse(source)))
def _git(*args: str) -> subprocess.CompletedProcess:
return subprocess.run(["git", *args], capture_output=True, text=True, cwd=REPO_ROOT)
def head_source(path: str) -> str | None:
"""取该文件在 HEAD 里的内容;返回 None 表示 HEAD 里没有它(新文件)。"""
r = _git("show", f"HEAD:{path}")
return r.stdout if r.returncode == 0 else None
def changed_python_files() -> list[str]:
r = _git("diff", "--name-only", "HEAD")
return [p for p in r.stdout.split() if p.endswith(".py")]
def staged_python_files() -> list[str]:
r = _git("diff", "--cached", "--name-only")
return [p for p in r.stdout.split() if p.endswith(".py")]
def first_diff(old: str, new: str) -> str:
"""给出一段人能看懂的差异上下文。"""
matcher = difflib.SequenceMatcher(None, old, new, autojunk=False)
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag != "equal":
return (f" 结构差异类型: {tag}\n"
f" 改前: ...{old[max(0, i1 - 70):i2 + 70]}...\n"
f" 改后: ...{new[max(0, j1 - 70):j2 + 70]}...")
return " (未找到差异片段——理论上不该走到这里)"
def main() -> int:
ap = argparse.ArgumentParser(
description="证明一次改动只动了注释/字符串,没有改逻辑",
)
ap.add_argument("paths", nargs="*", help="要检查的文件;留空 = 所有改过的 .py")
ap.add_argument("--staged", action="store_true", help="只检查已 git add 的文件")
args = ap.parse_args()
paths = args.paths or (staged_python_files() if args.staged else changed_python_files())
if not paths:
print("没有需要检查的 .py 文件(工作区是干净的?)。")
return 0
ok, problems, skipped = 0, [], []
for path in paths:
disk = REPO_ROOT / path
if not disk.is_file():
skipped.append((path, "工作区里不存在(被删除了?)"))
continue
old = head_source(path)
if old is None:
skipped.append((path, "HEAD 里没有它(新增文件),没有基准可比"))
continue
try:
same = normalize(old) == normalize(disk.read_text(encoding="utf-8"))
except SyntaxError as exc:
problems.append((path, f" 语法错误,先修好再看:{exc}"))
continue
if same:
ok += 1
else:
problems.append((path, first_diff(normalize(old),
normalize(disk.read_text(encoding="utf-8")))))
print(f"通过(只改了文字): {ok}")
for path, why in skipped:
print(f"跳过 {path} —— {why}")
if problems:
print(f"\n!! 有 {len(problems)} 个文件动了文字之外的东西:")
for path, why in problems:
print(f" {path}")
print(why)
print("\n注意:如果差异只出现在 f-string 的插值顺序上,那是允许的(见脚本说明)。")
return 1
print("\n闸门通过:改动范围内没有逻辑变化。")
return 0
if __name__ == "__main__":
sys.exit(main())
+15 -15
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Convert Flexiv Elements .traj (text) -> arm_application YAML waypoints."""
"""把 Flexiv Elements 的 .traj(文本)转换成 arm_application 的 YAML 路点。"""
from __future__ import annotations
@@ -12,7 +12,7 @@ from typing import Any
def parse_flexiv_traj(text: str) -> list[dict[str, Any]]:
"""Parse Elements Studio scatter-point .traj file."""
"""解析 Elements Studio 导出的散点 .traj 文件。"""
blocks = re.split(r'pattern_params\s*\{', text)[1:]
waypoints: list[dict[str, Any]] = []
for block in blocks:
@@ -35,7 +35,7 @@ def parse_flexiv_traj(text: str) -> list[dict[str, Any]]:
def assign_times(waypoints: list[dict[str, Any]], min_seg: float = 3.0) -> None:
"""Cumulative time_from_start from joint-space segment length."""
"""按关节空间的段长累加出 time_from_start。"""
t = 0.0
prev = None
for wp in waypoints:
@@ -44,7 +44,7 @@ def assign_times(waypoints: list[dict[str, Any]], min_seg: float = 3.0) -> None:
t += min_seg
else:
delta = max(abs(a - b) for a, b in zip(pos, prev))
# ~4 s per radian max joint move, floored at min_seg
# 关节最大运动量每弧度约 4 秒,且不低于 min_seg
t += max(min_seg, delta * 4.0)
wp['time'] = round(t, 2)
prev = pos
@@ -65,17 +65,17 @@ def to_yaml(
pos_line = ', '.join(f'{v:.6f}' for v in flat)
time_line = ', '.join(str(t) for t in times)
names = ', '.join(f' # {wp.get("name", "")}' if i == 0 else '' for i, wp in enumerate(waypoints))
_ = names # names kept in comments below
_ = names # 名称保留在下面的注释里
lines = [
'# Auto-converted from Flexiv Elements .traj (joints: deg -> rad)',
'# ROS params: flat waypoint_positions + waypoint_times (no nested dicts)',
'# Edit velocity_scale / loop_count as needed (no rebuild).',
'# 由 Flexiv Elements .traj 自动转换而来(关节角:度 -> 弧度)',
'# ROS 参数:扁平的 waypoint_positions + waypoint_times(不使用嵌套字典)',
'# 按需修改 velocity_scale / loop_count(无需重新编译)。',
'run_joint_trajectory:',
' ros__parameters:',
f' joint_prefix: "{joint_prefix}"',
' joint_names: [joint1, joint2, joint3, joint4, joint5, joint6, joint7]',
f' velocity_scale: {velocity_scale}',
' loop_count: 1 # 1=once, 0=forever, N=repeat N',
' loop_count: 1 # 1=执行一次,0=无限循环,N=重复 N 次',
' pause_between_loops: 0.5',
f' waypoint_times: [{time_line}]',
' waypoint_positions: [',
@@ -89,16 +89,16 @@ def to_yaml(
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('input', type=Path, help='Flexiv Elements .traj file')
ap.add_argument('-o', '--output', type=Path, help='Output YAML path')
ap.add_argument('--prefix', default='Rizon4-063989_', help='Joint name prefix (site SN + _)')
ap.add_argument('--min-seg', type=float, default=3.0, help='Min seconds per segment')
ap.add_argument('input', type=Path, help='Flexiv Elements 的 .traj 文件')
ap.add_argument('-o', '--output', type=Path, help='输出的 YAML 路径')
ap.add_argument('--prefix', default='Rizon4-063989_', help='关节名前缀(现场 SN + _)')
ap.add_argument('--min-seg', type=float, default=3.0, help='每段的最小秒数')
args = ap.parse_args()
text = args.input.read_text(encoding='utf-8')
wps = parse_flexiv_traj(text)
if not wps:
print('ERROR: no waypoints parsed', file=sys.stderr)
print('错误:没有解析到任何路点', file=sys.stderr)
return 1
assign_times(wps, min_seg=args.min_seg)
yaml_text = to_yaml(wps, joint_prefix=args.prefix)
@@ -107,7 +107,7 @@ def main() -> int:
if out:
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(yaml_text, encoding='utf-8')
print(f'Wrote {len(wps)} waypoints -> {out}')
print(f'已写入 {len(wps)} 个路点 -> {out}')
else:
print(yaml_text)
return 0
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Convert Flexiv Elements .traj → arm_application YAML (standard entry for all trajectories).
# Usage:
# 把 Flexiv Elements 的 .traj 转成 `arm_application` 的 YAML(所有轨迹的统一入口)。
# 用法:
# ./scripts/convert_traj.sh ~/文档/my_task.traj
# ./scripts/convert_traj.sh ~/文档/my_task.traj -o /path/to/out.yaml
# ./scripts/convert_traj.sh ~/文档/my_task.traj --min-seg 2.5 --no-copy
+4 -4
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Source this workspace environment (conda + ROS + flexiv libs).
# Usage: source scripts/env.sh
# 加载本工作区环境(conda + ROS + flexiv 库)。
# 用法:source scripts/env.sh
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [[ -f /opt/ros/humble/setup.bash ]]; then
@@ -33,7 +33,7 @@ fi
# shellcheck disable=SC1091
source "${ROOT}/install/setup.bash"
export LD_LIBRARY_PATH="${HOME}/flexiv_install/lib:${LD_LIBRARY_PATH:-}"
# Pip TensorRT (tensorrt-cu12) — needed to load YOLO .engine
# pip 版 TensorRT(tensorrt-cu12)—— 加载 YOLO `.engine` 需要它
_TRT_SITE="${CONDA_PREFIX:-${HOME}/anaconda3/envs/ros_flexiv}/lib/python3.11/site-packages"
for _d in "${_TRT_SITE}/tensorrt" "${_TRT_SITE}/tensorrt_libs" "${_TRT_SITE}/nvidia/tensorrt/lib"; do
if [[ -d "${_d}" ]]; then
@@ -44,6 +44,6 @@ unset _d _TRT_SITE
export YOLO_AUTOINSTALL=false
export ROS2_WS="${ROOT}"
# Master site config → SITE_* (YOLO weights, camera, robot SN, …)
# 主站点配置 → `SITE_*`(YOLO 权重、相机、机器人 SN……)
# shellcheck disable=SC1091
source "${ROOT}/scripts/lib/load_site_config.sh" 2>/dev/null || true
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Clone official Flexiv ROS 2 stack for Rizon (RDK v1.x → humble-v1 branch).
# 克隆 Flexiv 官方 ROS 2 栈(Rizon 用,RDK v1.x → humble-v1 分支)。
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -9,7 +9,7 @@ BRANCH="${FLEXIV_ROS2_BRANCH:-humble-v1}"
mkdir -p "${SRC}"
cd "${SRC}"
# Always ensure official URDF package is present (meshes + xacro)
# 无论如何都要保证官方 URDF 包存在(mesh + xacro)
if [[ ! -d flexiv_description ]]; then
echo "Fetching flexiv_description (${BRANCH})..."
if ! git clone --depth 1 -b "${BRANCH}" https://github.com/flexivrobotics/flexiv_description.git; then
@@ -58,7 +58,7 @@ if ! command -v vcs >/dev/null 2>&1; then
exit 1
fi
# Prefer local Flexiv_Rizon4 RDK (avoids GitHub clone when present)
# 优先用本地 Flexiv_Rizon4 里的 RDK(有本地副本时就不去 GitHub 克隆)
LOCAL_FLEXIV="${LOCAL_FLEXIV:-$HOME/文档/Flexiv_Rizon4}"
if [[ -d "${LOCAL_FLEXIV}/flexiv_rdk" ]]; then
echo "Using local RDK: ${LOCAL_FLEXIV}/flexiv_rdk"
@@ -73,7 +73,7 @@ else
fi
fi
# Always import remaining deps from repos (description already handled; skip-existing)
# 其余依赖一律从 .repos 导入(description 已单独处理;skip-existing 跳过已有)
vcs import "${SRC}" < "${REPOS_FILE}" --recursive --skip-existing || true
cat <<EOF
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Clone Flexiv Isaac Sim integration repos into thirdparty/
# 把 Flexiv Isaac Sim 集成仓库克隆到 thirdparty/
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -23,7 +23,7 @@ clone_or_update https://github.com/flexivrobotics/flexiv_sim_plugin.git flexiv_s
clone_or_update https://github.com/flexivrobotics/isaac_sim_ws.git isaac_sim_ws
clone_or_update https://github.com/flexivrobotics/flexiv_description.git flexiv_description
# Optional: RDK sources next to sim (if not already under src/)
# 可选:在仿真目录旁放一份 RDK 源码(如果 src/ 下还没有)
if [[ ! -d "${ROOT}/src/flexiv_rdk" ]]; then
clone_or_update https://github.com/flexivrobotics/flexiv_rdk.git flexiv_rdk
fi
+2 -2
View File
@@ -15,7 +15,7 @@ source "${ROOT}/scripts/lib/load_site_config.sh" 2>/dev/null || true
PC_IP_PRIMARY="${SITE_PC_IP:-192.168.2.10}"
PC_ADDRS=("192.168.2.10/24" "192.168.100.10/24")
ROBOT_IPS=("${SITE_ROBOT_IP:-192.168.2.100}")
# Catalog extras (second arm)
# catalog 里的额外项(第二条臂)
if [[ -n "${SITE_ROBOT_CATALOG:-}" ]]; then
:
fi
@@ -132,7 +132,7 @@ for ip in 192.168.2.100 192.168.101.1 192.168.2.101 "${SITE_ROBOT_IP:-}"; do
[[ -z "$ip" ]] && continue
ping_one "$ip" && ok=1 || true
done
# unique
# 去重
echo
if ping -c 1 -W 1 "${SITE_ROBOT_IP:-192.168.2.100}" >/dev/null 2>&1; then
echo "OK — 当前臂 ${SITE_ROBOT_SN:-} @ ${SITE_ROBOT_IP} 可达。RDK 用 SN 连接。"
+1 -1
View File
@@ -88,7 +88,7 @@ REGEX_REPLACEMENTS: list[tuple[str, str]] = [
def render(site_text: str) -> str:
"""Turn the live config text into the anonymised template text."""
"""把现场配置文本转换成匿名化后的模板文本。"""
out = site_text
for old, new in LITERAL_REPLACEMENTS:
out = out.replace(old, new)
+15 -15
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env bash
# Move Rizon4 to a named pose (default: home) and FORCE stop.
# 把 Rizon4 移动到指定命名的位姿(默认:home)并强制停止。
#
# Critical: visual_servo.launch / servo_node must be killed first, otherwise
# they keep streaming JointTrajectory and the arm never settles.
# 关键:必须先杀掉 visual_servo.launch / servo_node,否则
# 它们会一直发 JointTrajectory,机械臂永远稳定不下来。
#
# Usage:
# 用法:
# bash ~/ros2/scripts/go_home.sh
# bash ~/ros2/scripts/go_home.sh --pose ready --speed 0.3
@@ -39,7 +39,7 @@ while [[ $# -gt 0 ]]; do
--pose) POSE="${2:?}"; shift 2 ;;
--speed) SPEED="${2:?}"; shift 2 ;;
--keep-servo) KEEP_SERVO=1; shift ;;
--stop-track) shift ;; # legacy
--stop-track) shift ;; # 历史遗留
-h|--help) usage; exit 0 ;;
*) echo "Unknown: $1" >&2; usage >&2; exit 1 ;;
esac
@@ -56,10 +56,10 @@ SPEED="${SPEED:-${SITE_GO_HOME_SPEED:-0.25}}"
kill_streamers() {
echo "=== killing visual-servo streamers ==="
# Prefer disable control before kill (if node still up)
# 能先禁用控制就禁用(如果节点还活着),再动手 kill
ros2 param set /servo_node enable_control false >/dev/null 2>&1 || true
# Kill launch parent first, then executables (SIGTERM then SIGKILL)
# 先杀 launch 父进程,再杀可执行文件(先 SIGTERM 再 SIGKILL)
local patterns=(
'visual_servo.launch.py'
'/lib/arm_application/servo_node'
@@ -115,7 +115,7 @@ n.destroy_node(); rclpy.shutdown()
PY
}
# Strong settle via controller ACTION (not topic — matches JTC reliably)
# 用控制器的 ACTION 做强力稳定(不走话题 —— 这样能可靠地匹配 JTC)
settle_hold_action() {
local pose="$1"
python3 - "$pose" <<'PY'
@@ -160,7 +160,7 @@ def spin_until(pred, timeout=3.0):
spin_until(lambda: js_box[0] is not None, 3.0)
# Two-point: current -> target (or target hold), BOTH with zero velocity
# 两点轨迹:当前位姿 -> 目标位姿(或保持在目标位姿),两点速度都为零
q_now = list(target)
if js_box[0] is not None:
m = dict(zip(js_box[0].name, js_box[0].position))
@@ -172,14 +172,14 @@ if js_box[0] is not None:
def send_hold(q, T=0.40):
traj = JointTrajectory()
traj.joint_names = list(JOINTS)
# start
# 起点
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
# end same pose, still zero vel (pin)
# 终点是同一姿势,速度仍为零(钉住)
p1 = JointTrajectoryPoint()
p1.positions = [float(v) for v in q]
p1.velocities = [0.0] * 7
@@ -203,11 +203,11 @@ def send_hold(q, T=0.40):
spin_until(lambda: rfut.done(), T + 3.0)
return True
# 1) snap toward named pose with zero end vel
# 1) 先朝命名位姿快速靠拢,末端速度为零
print(f" action hold → {name} (zero vel)")
send_hold(target, T=0.60)
time.sleep(0.4)
# 2–4) re-pin measured pose several times (align JTC open-loop cmd to encoder)
# 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:
@@ -218,7 +218,7 @@ for i in range(3):
send_hold(q_pin, T=0.45)
time.sleep(0.25)
# Wait until |qd| small
# 等到 |qd| 足够小
print(" waiting for joints to stop ...")
t0 = time.monotonic()
while time.monotonic() - t0 < 5.0:
@@ -270,7 +270,7 @@ sleep "${WAIT_SEC}"
echo "=== settle (action, vel=0) ==="
settle_hold_action "${POSE}"
# Final check: servo must still be dead
# 最后确认:servo 必须仍然是死的
if pgrep -f '/lib/arm_application/servo_node' >/dev/null 2>&1; then
echo "WARNING: servo_node came back — killing again"
kill_streamers
+3 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Offline-friendly Flexiv RDK v1.9.1 install for ROS (humble-v1 / flexiv_hardware).
# CMakeLists already supports FLEXIV_RDK_OFFLINE_LIB / vendor/rdk_libs/.
# 面向离线场景的 Flexiv RDK v1.9.1 安装脚本(供 ROS 的 humble-v1 / `flexiv_hardware` 使用)。
# CMakeLists 已经支持 `FLEXIV_RDK_OFFLINE_LIB` / `vendor/rdk_libs/`。
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -11,7 +11,7 @@ EXPECTED_HASH="$(tr -d '[:space:]' < "${RDK_SRC}/lib/${LIB_NAME}.sha256")"
VENDOR_LIB="${ROOT}/vendor/rdk_libs/${LIB_NAME}"
JOBS="${JOBS:-$(nproc)}"
# Prefer conda ros_flexiv toolchain (cmake/g++ often only there)
# 优先用 conda 的 ros_flexiv 工具链(cmake/g++ 往往只在那里有)
if ! command -v cmake >/dev/null 2>&1; then
# shellcheck disable=SC1091
source "${HOME}/anaconda3/etc/profile.d/conda.sh" 2>/dev/null \
+43 -43
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Load ~/ros2/config/site.yaml and print bash export lines or JSON.
"""读取 ``~/ros2/config/site.yaml``,打印 bash 的 export 行或 JSON。
Usage:
用法:
eval "$(python3 ~/ros2/scripts/lib/export_site_env.py)"
python3 ~/ros2/scripts/lib/export_site_env.py --json
python3 ~/ros2/scripts/lib/export_site_env.py --print
@@ -35,18 +35,18 @@ def site_path() -> Path:
def load_site(path: Path | None = None) -> dict:
path = path or site_path()
if not path.is_file():
raise FileNotFoundError(f"site config not found: {path}")
raise FileNotFoundError(f"找不到 site 配置: {path}")
text = path.read_text(encoding="utf-8")
if yaml is None:
raise RuntimeError("PyYAML required: pip/conda install pyyaml")
raise RuntimeError("需要 PyYAML:pip/conda install pyyaml")
data = yaml.safe_load(text) or {}
if not isinstance(data, dict):
raise ValueError(f"invalid site.yaml: {path}")
raise ValueError(f"无效的 site.yaml: {path}")
return data
def list_robots(data: dict) -> list[dict]:
"""Catalog entries from robot.robots (list or SN-keyed map)."""
"""从 robot.robots 取清单条目(列表或按 SN 索引的映射)。"""
raw = (data.get("robot") or {}).get("robots") or []
out: list[dict] = []
if isinstance(raw, dict):
@@ -90,7 +90,7 @@ def _deep_merge(base: dict, overlay: dict) -> dict:
def normalize_robot_sn(data: dict, want: str) -> str:
"""Map 89/91/left/right/label → full Flexiv SN."""
"""把 89/91/left/right/label 映射成完整的 Flexiv SN。"""
w = (want or "").strip()
if not w:
return w
@@ -109,7 +109,7 @@ def normalize_robot_sn(data: dict, want: str) -> str:
def resolve_robot(data: dict, sn: str | None = None) -> dict:
"""Active arm: ROBOT_SN env overrides robot.sn; catalog fills ip/iface/frames."""
"""当前生效的机械臂:环境变量 ``ROBOT_SN`` 覆盖 robot.sn;清单补齐 ip/iface/frames。"""
robot = dict(data.get("robot") or {})
catalog = list_robots(data)
want = normalize_robot_sn(
@@ -146,7 +146,7 @@ def resolve_robot(data: dict, sn: str | None = None) -> dict:
def apply_active_arm(data: dict, sn: str | None = None) -> tuple[dict, dict]:
"""Merge robots[] camera/gripper/paths/servo onto site defaults for the active SN."""
"""把 robots[] 里的 camera/gripper/paths/servo 合并到当前 SN 对应的 site 默认值上。"""
robot = resolve_robot(data, sn)
merged = dict(data)
for key in _ARM_SECTION_KEYS:
@@ -157,14 +157,14 @@ def apply_active_arm(data: dict, sn: str | None = None) -> tuple[dict, dict]:
def camera_ns(data: dict) -> str:
"""Topic namespace for the active wrist camera (d405_left / d405_right)."""
"""当前腕部相机(d405_left / d405_right)的话题命名空间。"""
cam = data.get("camera") or {}
name = str(cam.get("name") or "d405").strip().strip("/")
return name or "d405"
def camera_frames(data: dict) -> tuple[str, str, str]:
"""(namespace, optical_frame, depth_topic) for the active arm."""
"""当前机械臂的(命名空间, 光学坐标系, 深度话题)。"""
ns = camera_ns(data)
yolo = data.get("yolo") or {}
frame = str(yolo.get("camera_frame") or "").strip()
@@ -177,7 +177,7 @@ def camera_frames(data: dict) -> tuple[str, str, str]:
def flatten(data: dict) -> dict:
"""Flat SITE_* env map for bash scripts."""
"""供 bash 脚本使用的扁平 ``SITE_*`` 环境变量映射。"""
data, robot = apply_active_arm(data)
paths = data.get("paths") or {}
cam = data.get("camera") or {}
@@ -341,7 +341,7 @@ def flatten(data: dict) -> dict:
def gripper_overrides(data: dict) -> dict:
"""ROS parameter dict for dh_pgea_node (DH Modbus, not Flexiv gripper)."""
"""供 dh_pgea_node 使用的 ROS 参数字典(DH Modbus,不是 Flexiv 夹爪)。"""
data, _robot = apply_active_arm(data)
g = data.get("gripper") or {}
return {
@@ -362,7 +362,7 @@ def gripper_overrides(data: dict) -> dict:
def perception_overrides(data: dict) -> dict:
"""ROS parameter dict for perception_node."""
"""供 perception_node 使用的 ROS 参数字典。"""
data, _robot = apply_active_arm(data)
paths = data.get("paths") or {}
yolo = data.get("yolo") or {}
@@ -415,7 +415,7 @@ def perception_overrides(data: dict) -> dict:
def servo_overrides(data: dict) -> dict:
"""ROS parameter dict for servo_node."""
"""供 servo_node 使用的 ROS 参数字典。"""
data, robot = apply_active_arm(data)
paths = data.get("paths") or {}
yolo = data.get("yolo") or {}
@@ -431,7 +431,7 @@ def servo_overrides(data: dict) -> dict:
"arm_label": str(robot.get("label") or robot.get("arm") or ""),
"z_ref_m": float(servo.get("z_ref_m") or yolo.get("z_ref_m") or 0.40),
}
# Pass through remaining servo keys that match node params
# 透传其余与节点参数同名的 servo 键
keys = [
"control_hz", "watchdog_timeout_sec", "lost_timeout_sec", "stale_hold_sec",
"max_extrapolate_sec", "predict_enable", "predict_sec",
@@ -460,7 +460,7 @@ def main() -> int:
ap.add_argument("--json", action="store_true")
ap.add_argument("--print", action="store_true", dest="do_print")
ap.add_argument("--config", default="")
ap.add_argument("--sn", "--arm", dest="sn", default="", help="89|91|left|right|full SN")
ap.add_argument("--sn", "--arm", dest="sn", default="", help="89|91|left|right|完整 SN")
args = ap.parse_args()
if args.sn:
os.environ["ROBOT_SN"] = str(args.sn)
@@ -471,35 +471,35 @@ def main() -> int:
print(json.dumps({"site": data, "env": flat}, ensure_ascii=False, indent=2))
return 0
if args.do_print:
print(f"site config: {path}")
print(f"site 配置: {path}")
print(
f" robot SN : {flat['SITE_ROBOT_SN']}"
f" 机械臂 SN : {flat['SITE_ROBOT_SN']}"
f"{(' (' + flat['SITE_ROBOT_LABEL'] + ')') if flat.get('SITE_ROBOT_LABEL') else ''}"
f" arm={flat.get('SITE_ROBOT_ARM') or '?'}"
)
print(f" catalog : {flat.get('SITE_ROBOT_CATALOG') or '(none)'}")
print(f" robot IP : {flat['SITE_ROBOT_IP']} pc={flat['SITE_PC_IP']} iface={flat.get('SITE_NET_IFACE') or '?'}")
print(f" frames : prefix={flat['SITE_JOINT_PREFIX']} ee={flat['SITE_EE_FRAME']}")
print(f" tool name : {flat.get('SITE_ROBOT_TOOL_NAME') or '(keep current)'}")
print(f" 清单 : {flat.get('SITE_ROBOT_CATALOG') or '(无)'}")
print(f" 机械臂 IP : {flat['SITE_ROBOT_IP']} pc={flat['SITE_PC_IP']} iface={flat.get('SITE_NET_IFACE') or '?'}")
print(f" 坐标系 : prefix={flat['SITE_JOINT_PREFIX']} ee={flat['SITE_EE_FRAME']}")
print(f" 工具名 : {flat.get('SITE_ROBOT_TOOL_NAME') or '(保持当前)'}")
catalog_sns = [str(r.get("sn") or "") for r in list_robots(data)]
if catalog_sns and flat["SITE_ROBOT_SN"] not in catalog_sns:
print(
f" WARNING: robot.sn {flat['SITE_ROBOT_SN']!r} is not in robots[]",
f" 警告:robot.sn {flat['SITE_ROBOT_SN']!r} 不在 robots[] 里",
file=sys.stderr,
)
print(f" yolo weights : {flat['SITE_YOLO_WEIGHTS']}")
print(f" resolved : {flat['SITE_YOLO_WEIGHTS_RESOLVED']}")
print(f" handeye : {flat['SITE_HANDEYE_FILE']}")
print(f" handeye wp : {flat['SITE_HANDEYE_WAYPOINTS']}")
print(f" handeye dir : {flat['SITE_HANDEYE_WORKDIR']}")
print(f" named poses : {flat.get('SITE_NAMED_POSES_FILE')}")
print(f" YOLO 权重 : {flat['SITE_YOLO_WEIGHTS']}")
print(f" 解析后 : {flat['SITE_YOLO_WEIGHTS_RESOLVED']}")
print(f" 手眼 : {flat['SITE_HANDEYE_FILE']}")
print(f" 手眼路点 : {flat['SITE_HANDEYE_WAYPOINTS']}")
print(f" 手眼目录 : {flat['SITE_HANDEYE_WORKDIR']}")
print(f" 命名位姿 : {flat.get('SITE_NAMED_POSES_FILE')}")
print(
" board : "
" 标定板 : "
f"{flat['SITE_HANDEYE_BOARD_COLS']}x{flat['SITE_HANDEYE_BOARD_ROWS']} "
f"inner, square={flat['SITE_HANDEYE_SQUARE_M']} m"
f"内角点, square={flat['SITE_HANDEYE_SQUARE_M']} m"
)
print(
f" camera : serial={flat['SITE_CAM_SERIAL'] or '(empty)'} "
f" 相机 : serial={flat['SITE_CAM_SERIAL'] or '(空)'} "
f"{flat['SITE_CAM_WIDTH']}x{flat['SITE_CAM_HEIGHT']}@{flat['SITE_CAM_FPS']} "
f"ae={flat.get('SITE_CAM_AUTO_EXPOSURE')} "
f"exp={flat.get('SITE_CAM_EXPOSURE_US')}us "
@@ -507,26 +507,26 @@ def main() -> int:
f"ae_limit={flat.get('SITE_CAM_AE_LIMIT_US')}us emitter={flat.get('SITE_CAM_EMITTER')}"
)
print(
f" catalog cam : leftD405={flat.get('SITE_D405_LEFT_SERIAL')} "
f" 清单相机 : leftD405={flat.get('SITE_D405_LEFT_SERIAL')} "
f"rightD405={flat.get('SITE_D405_RIGHT_SERIAL')} "
f"D435={flat.get('SITE_D435_SERIAL')}"
)
print(
f" gripper : {flat.get('SITE_GRIP_MODEL')} port={flat.get('SITE_GRIP_PORT')}"
f" 夹爪 : {flat.get('SITE_GRIP_MODEL')} port={flat.get('SITE_GRIP_PORT')}"
)
print(
f" hole_detect : backend={flat.get('SITE_HOLE_BACKEND')} "
f"weights={flat.get('SITE_HOLE_WEIGHTS')}"
)
print(
f" tactile : backend={flat.get('SITE_TACTILE_BACKEND')} "
f" 触觉 : backend={flat.get('SITE_TACTILE_BACKEND')} "
f"L={flat.get('SITE_TACTILE_LEFT')} R={flat.get('SITE_TACTILE_RIGHT')} "
f"cfg={flat.get('SITE_TACTILE_CONFIG')}"
)
print(f" go_home : pose={flat['SITE_GO_HOME_POSE']} speed={flat['SITE_GO_HOME_SPEED']}")
he_path = Path(flat["SITE_HANDEYE_FILE"]).expanduser()
if not he_path.is_file():
print(f" WARNING: hand-eye file missing: {he_path}", file=sys.stderr)
print(f" 警告:手眼文件缺失: {he_path}", file=sys.stderr)
else:
try:
he_data = yaml.safe_load(he_path.read_text(encoding="utf-8")) or {}
@@ -536,28 +536,28 @@ def main() -> int:
he_data.get("uncalibrated") or "T_4x4" not in he_data
):
print(
f" WARNING: {he_path.name} is not calibrated for this arm "
f" 警告:{he_path.name} 没有针对该机械臂标定 "
f"(run_handeye.sh teach/calibrate --arm {flat.get('SITE_ROBOT_ARM') or '91'})",
file=sys.stderr,
)
if "063991" in flat["SITE_ROBOT_SN"] and flat.get("SITE_CAM_SERIAL") == flat.get("SITE_D405_LEFT_SERIAL"):
print(
" WARNING: 91 is using the LEFT wrist D405 serial — check robots[].camera.serial",
" 警告:91 用的是左腕 D405 的序列号 — 请检查 robots[].camera.serial",
file=sys.stderr,
)
if "RGI" in str(flat.get("SITE_GRIP_MODEL") or ""):
print(
" NOTE: 本臂是 RGI。CLI: bash ~/ros2/scripts/run_gripper.sh --arm 91 --cli open|rotate",
" 提示:本臂是 RGI。CLI: bash ~/ros2/scripts/run_gripper.sh --arm 91 --cli open|rotate",
file=sys.stderr,
)
w = Path(flat["SITE_YOLO_WEIGHTS"]).expanduser()
if not w.is_file():
print(f" WARNING: weights file missing: {w}", file=sys.stderr)
print(f" 警告:权重文件缺失: {w}", file=sys.stderr)
return 2
return 0
# bash exports
# bash 导出
for k, v in flat.items():
# escape for eval
# 为 eval 转义
vv = str(v).replace("'", "'\"'\"'")
print(f"export {k}='{vv}'")
return 0
+3 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Load ~/ros2/config/site.yaml into SITE_* environment variables.
# Usage (from other scripts):
# 把 ~/ros2/config/site.yaml 加载成 `SITE_*` 环境变量。
# 用法(在其它脚本里):
# source "${ROOT}/scripts/lib/load_site_config.sh"
_SITE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -13,7 +13,7 @@ else
# shellcheck disable=SC1090
eval "$(python3 "${_SITE_EXPORTER}" --config "${_SITE_FILE}")"
export SITE_CONFIG="${_SITE_FILE}"
# Resolve 89/91/left/right aliases to the full Flexiv SN for RDK.
# 把 89/91/left/right 别名解析成完整的 Flexiv SN,供 RDK 使用。
if [[ -n "${SITE_ROBOT_SN:-}" ]]; then
export ROBOT_SN="${SITE_ROBOT_SN}"
fi
+3 -3
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env bash
# Parse --sn / --arm from argv. Must be sourced BEFORE load_site_config.sh
# so ROBOT_SN is visible to export_site_env.py.
# 从 argv 里解析 --sn / --arm。必须在 `load_site_config.sh` 之前 source,
# 这样 `ROBOT_SN` 才能被 export_site_env.py 看到。
#
# source "${ROOT}/scripts/lib/parse_arm_args.sh"
# parse_arm_args "$@"
# set -- "${PARSE_ARM_REST[@]}"
#
# Aliases (89|91|left|right) are resolved in export_site_env.py.
# 别名(89|91|left|right)在 export_site_env.py 里解析。
parse_arm_args() {
PARSE_ARM_REST=()
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# List plugged RealSense cameras (serials used in site.yaml).
# 列出已插上的 RealSense 相机(序列号在 site.yaml 里使用)。
set -eo pipefail
python3 - <<'PY'
import pyrealsense2 as rs
+4 -4
View File
@@ -73,7 +73,7 @@ def format_positions_block(poses: list[list[float]], indent: str = " ") -> st
def merge(yaml_path: Path, poses: list[list[float]]) -> None:
text = yaml_path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
# Detect indent of waypoint_positions (2 or 4 spaces typical)
# 探测 waypoint_positions 的缩进(通常 2 空格或 4 空格)
m_ind = re.search(r"^([ \t]*)waypoint_positions:\s*\[", text, re.MULTILINE)
indent = m_ind.group(1) if m_ind else " "
block = format_positions_block(poses, indent=indent)
@@ -82,7 +82,7 @@ def merge(yaml_path: Path, poses: list[list[float]]) -> None:
rf"{re.escape(indent)}waypoint_positions:\s*\[[\s\S]*?{re.escape(indent)}\]",
re.MULTILINE,
)
# Also match a one-line empty list: waypoint_positions: []
# 也要匹配"单行空列表"的写法:waypoint_positions: []
one_line = re.compile(
rf"{re.escape(indent)}waypoint_positions:\s*\[\s*\]",
re.MULTILINE,
@@ -94,7 +94,7 @@ def merge(yaml_path: Path, poses: list[list[float]]) -> None:
else:
raise RuntimeError("waypoint_positions block not found in YAML")
# Keep num_poses in sync when present
# 顺手把 num_poses 同步成实际点数(如果原本就有这个键)
n = len(poses)
if re.search(r"^\s*num_poses:\s*\d+", new_text, re.MULTILINE):
new_text = re.sub(
@@ -105,7 +105,7 @@ def merge(yaml_path: Path, poses: list[list[float]]) -> None:
flags=re.MULTILINE,
)
else:
# Insert num_poses just before waypoint_positions
# 原本没有 num_poses,就插在 waypoint_positions 前面
new_text = new_text.replace(
block,
f"{indent}num_poses: {n}\n{block}",
+24 -24
View File
@@ -105,13 +105,13 @@ class SoftDrag:
robot.SetJointImpedance(g, soft)
log(f'SetJointImpedance {g} K={soft}')
except Exception as exc:
log(f'SetJointImpedance group {g}: {exc}')
log(f'SetJointImpedance 组 {g}: {exc}')
else:
n = max(7, len(self.latest_q))
try:
robot.SetJointImpedance([35.0] * n, [0.7] * n)
except Exception as exc:
log(f'SetJointImpedance flat: {exc}')
log(f'SetJointImpedance flat(不分组): {exc}')
self._stop.clear()
self._thread = threading.Thread(target=self._loop, name='soft-drag', daemon=True)
@@ -144,9 +144,9 @@ class SoftDrag:
n = len(q)
robot.SendJointPosition(q, [0.0] * n, [1.5] * n, [2.0] * n)
except Exception as exc:
log(f'soft loop: {exc}')
log(f'soft 循环: {exc}')
time.sleep(0.15)
# ~20 Hz — keep lock released most of the time
# 约 20 Hz —— 大部分时间让锁保持释放状态
for _ in range(10):
if self._stop.is_set():
return
@@ -158,7 +158,7 @@ class SoftDrag:
if th is not None:
th.join(timeout=4.0)
if th.is_alive():
log('soft loop still alive after join (will idle anyway)')
log('join 之后 soft 循环仍然存活(无论如何都会切到 IDLE)')
self._thread = None
@@ -168,7 +168,7 @@ def _ensure_operational(robot) -> Optional[str]:
def _idle_async(robot, lock: threading.Lock, flexivrdk, timeout: float = 4.0) -> bool:
"""Switch to IDLE without blocking the stdin loop forever."""
"""切到 IDLE,同时避免把 stdin 循环永久阻塞住。"""
done = threading.Event()
def run() -> None:
@@ -193,7 +193,7 @@ def _idle_async(robot, lock: threading.Lock, flexivrdk, timeout: float = 4.0) ->
threading.Thread(target=run, name='idle', daemon=True).start()
ok = done.wait(timeout)
if not ok:
log('IDLE/Stop timed out — stdin loop continues')
log('IDLE/Stop 超时 —— stdin 循环继续运行')
return ok
@@ -213,7 +213,7 @@ def main() -> int:
try:
msg = json.loads(line)
except Exception as exc: # noqa: BLE001
reply({'ok': False, 'error': f'bad json: {exc}'})
reply({'ok': False, 'error': f'JSON 解析失败: {exc}'})
continue
cmd = str(msg.get('cmd', '')).strip()
@@ -233,17 +233,17 @@ def main() -> int:
for args in attempts:
try:
robot = flexivrdk.Robot(*args)
log(f'connected {sn} args={args!r}')
log(f'已连接 {sn} args={args!r}')
last_err = None
break
except TypeError:
continue
except Exception as exc:
last_err = exc
log(f'connect {args!r}: {exc}')
log(f'连接 {args!r}: {exc}')
robot = None
if robot is None:
reply({'ok': False, 'error': str(last_err or 'connect failed')})
reply({'ok': False, 'error': str(last_err or '连接失败')})
continue
err = _ensure_operational(robot)
if err:
@@ -256,7 +256,7 @@ def main() -> int:
elif cmd == 'drag_on':
if robot is None:
reply({'ok': False, 'error': 'not connected'})
reply({'ok': False, 'error': '尚未连接'})
continue
err = _ensure_operational(robot)
if err:
@@ -274,7 +274,7 @@ def main() -> int:
reply({'ok': True, 'drag': True, 'mode': 'floating', 'primitive': name})
continue
except Exception as exc:
log(f'Floating failed, fallback soft: {exc}')
log(f'Floating 失败,退回 soft 模式: {exc}')
try:
if soft is not None:
@@ -284,18 +284,18 @@ def main() -> int:
if soft.latest_q:
cached_q = list(soft.latest_q)
drag_on = True
log('soft drag ON')
log('软拖拽已开启')
reply({
'ok': True,
'drag': True,
'mode': 'soft',
'hint': 'Push the arm gently; low joint impedance. Enter=photo (s=hold).',
'hint': '轻推机械臂即可,关节阻抗已调低。Enter=拍照(s=保持)。',
})
except Exception as exc:
reply({'ok': False, 'error': f'soft drag failed: {exc}'})
reply({'ok': False, 'error': f'软拖拽失败: {exc}'})
elif cmd == 'drag_off':
log('drag_off requested')
log('收到 drag_off 请求')
if soft is not None:
if soft.latest_q:
cached_q = list(soft.latest_q)
@@ -304,7 +304,7 @@ def main() -> int:
if robot is not None:
_idle_async(robot, rlock, flexivrdk, timeout=4.0)
drag_on = False
log('drag_off done')
log('drag_off 已完成')
reply({'ok': True, 'drag': False, 'q': cached_q})
elif cmd == 'joints':
@@ -314,14 +314,14 @@ def main() -> int:
elif len(cached_q) >= 7:
q = list(cached_q)
if q is None or len(q) < 7:
reply({'ok': False, 'error': 'no joint cache'})
reply({'ok': False, 'error': '没有关节缓存'})
continue
cached_q = q
reply({'ok': True, 'q': q, 'drag': drag_on})
elif cmd == 'enable':
if robot is None:
reply({'ok': False, 'error': '尚未 connect'})
reply({'ok': False, 'error': '尚未连接'})
continue
err = ensure_operational(robot)
reply({
@@ -332,7 +332,7 @@ def main() -> int:
elif cmd == 'clear_fault':
if robot is None:
reply({'ok': False, 'error': '尚未 connect'})
reply({'ok': False, 'error': '尚未连接'})
continue
err = rdk_clear_fault(robot)
if err:
@@ -349,7 +349,7 @@ def main() -> int:
elif cmd == 'switch_tool':
if robot is None:
reply({'ok': False, 'error': '尚未 connect'})
reply({'ok': False, 'error': '尚未连接'})
continue
want = str(msg.get('name', '')).strip()
@@ -428,9 +428,9 @@ def main() -> int:
return 0
else:
reply({'ok': False, 'error': f'unknown cmd {cmd}'})
reply({'ok': False, 'error': f'未知命令 {cmd}'})
except Exception as exc: # noqa: BLE001
log(f'cmd={cmd} error: {exc}')
log(f'cmd={cmd} 出错: {exc}')
reply({'ok': False, 'error': str(exc)})
return 0
+27 -27
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""Control Studio device GripperDahuan / GripperDahuanModbus via Flexiv RDK.
"""通过 Flexiv RDK 控制 Studio 设备 GripperDahuan / GripperDahuanModbus。
Must run under RDK Python (rdk_py310). Uses a *lite* RDK instance so it can
share the robot with run_real.sh (ros2_control holds the normal RDK).
必须在 RDK 的 Python(rdk_py310)下运行。使用 *lite*(轻量)RDK 实例,
以便和 run_real.sh 共用机器人(常规 RDK 被 ros2_control 占用)。
The controller talks RS485 itself (its own /dev/ttyUSB0), not the IPC serial.
控制器自己走 RS485(用自带的 /dev/ttyUSB0),不走工控机的串口。
/home/cc/rdk_py310/bin/python ~/ros2/scripts/rdk_gripper.py --list
/home/cc/rdk_py310/bin/python ~/ros2/scripts/rdk_gripper.py status
@@ -65,28 +65,28 @@ def _move(grip, params, width: float, vel: float, force: float) -> None:
vmax = float(params.max_vel)
if vmax <= 0:
vmax = 0.05
# Move velocity cannot be 0 (RDK).
# 移动速度不能为 0(RDK 限制)。
vel = _clamp(vel, max(vmin, 1e-4), vmax)
force = _clamp(force, float(params.min_force), float(params.max_force))
width = _clamp(width, float(params.min_width), float(params.max_width))
print(f"Move width={width:.4f} m vel={vel:.4f} m/s force={force:.1f} N")
print(f"移动 width={width:.4f} m vel={vel:.4f} m/s force={force:.1f} N")
grip.Move(width, vel, force)
def main() -> int:
ap = argparse.ArgumentParser(description="Flexiv RDK gripper (GripperDahuanModbus)")
ap = argparse.ArgumentParser(description="Flexiv RDK 夹爪(GripperDahuanModbus)")
ap.add_argument("--sn", default="")
ap.add_argument("--iface", default="")
ap.add_argument("--device", default="", help="Studio Device name")
ap.add_argument("--force-n", type=float, default=0.0, help="force limit [N]; 0=mid")
ap.add_argument("--vel", type=float, default=0.0, help="speed [m/s]; 0=half max")
ap.add_argument("--device", default="", help="Studio 设备名")
ap.add_argument("--force-n", type=float, default=0.0, help="力上限 [N];0=取中值")
ap.add_argument("--vel", type=float, default=0.0, help="速度 [m/s];0=最大速度的一半")
ap.add_argument(
"cmd",
nargs="?",
default="status",
choices=["list", "status", "init", "open", "close", "pos", "enable"],
)
ap.add_argument("value", nargs="?", default="", help="pos: meters or 0–1000")
ap.add_argument("value", nargs="?", default="", help="pos: 米,或 0–1000")
args = ap.parse_args()
sys.path.insert(0, str(ROOT / "scripts" / "lib"))
@@ -99,18 +99,18 @@ def main() -> int:
iface = (args.iface or robot_cfg.get("iface") or "").strip()
device = (args.device or g.get("device_name") or "GripperDahuanModbus").strip()
if not sn:
print("ERROR: robot SN missing", file=sys.stderr)
print("错误:没拿到机械臂序列号(SN)", file=sys.stderr)
return 2
import flexivrdk
print(f"=== RDK lite gripper SN={sn} device={device} iface={iface or 'auto'} ===")
print(f"=== RDK lite 夹爪 SN={sn} device={device} iface={iface or 'auto'} ===")
robot = connect_lite(sn, iface)
dev = flexivrdk.Device(robot)
names = list(dev.list() or [])
if args.cmd == "list":
print("Studio devices:")
print("Studio 设备列表:")
for n in names:
try:
en = dev.enabled(n)
@@ -123,7 +123,7 @@ def main() -> int:
if device not in names:
print(
f"ERROR: device {device!r} not in Studio pool.\n available: {', '.join(names)}",
f"错误:设备 {device!r} 不在 Studio 设备池里。\n 可用: {', '.join(names)}",
file=sys.stderr,
)
return 1
@@ -133,7 +133,7 @@ def main() -> int:
)
if not dev.connected(device):
print(
"WARN: connected=False — 控制器自己的 /dev/ttyUSB0 没接到爪。\n"
"警告:connected=False —— 控制器自己的 /dev/ttyUSB0 没接到夹爪。\n"
" USB–RS485 要插 Flexiv 机箱 USB(不是工控机),A/B + 24V 到 PGEA。\n"
" 或用法兰工具口 485(腕部白绿+/绿-)。"
)
@@ -142,14 +142,14 @@ def main() -> int:
if args.cmd in ("enable", "status", "init", "open", "close", "pos"):
try:
grip.Enable(device)
print(f"Enable [{device}] OK")
print(f"使能 [{device}] 成功")
except Exception as exc:
print(f"ERROR: Enable failed: {exc}", file=sys.stderr)
print(f"错误:使能失败: {exc}", file=sys.stderr)
return 1
params = grip.params()
print(" params:", _fmt_params(params))
print(" states:", _fmt_states(grip.states()))
print(" 参数:", _fmt_params(params))
print(" 状态:", _fmt_states(grip.states()))
force = float(args.force_n or g.get("force_n") or 0.0)
if force <= 0:
@@ -164,27 +164,27 @@ def main() -> int:
return 0
if args.cmd == "init":
print("Init (fingers home, ~10 s) …")
print("初始化(手指回零,约 10 秒)…")
grip.Init()
time.sleep(2.0)
print(" states:", _fmt_states(grip.states()))
print(" 状态:", _fmt_states(grip.states()))
return 0
if args.cmd == "open":
_move(grip, params, float(params.max_width), vel, force)
time.sleep(0.4)
print(" states:", _fmt_states(grip.states()))
print(" 状态:", _fmt_states(grip.states()))
return 0
if args.cmd == "close":
_move(grip, params, float(params.min_width), vel, force)
time.sleep(0.4)
print(" states:", _fmt_states(grip.states()))
print(" 状态:", _fmt_states(grip.states()))
return 0
if args.cmd == "pos":
if args.value == "":
print("usage: pos <meters|0-1000>", file=sys.stderr)
print("用法: pos <米|0-1000>", file=sys.stderr)
return 2
raw = float(args.value)
if raw > 1.5:
@@ -194,7 +194,7 @@ def main() -> int:
width = raw
_move(grip, params, width, vel, force)
time.sleep(0.4)
print(" states:", _fmt_states(grip.states()))
print(" 状态:", _fmt_states(grip.states()))
return 0
return 0
@@ -203,5 +203,5 @@ if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
print(f"错误:{exc}", file=sys.stderr)
raise SystemExit(1)
+38 -38
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Switch the Flexiv active tool (TCP + payload) to a Studio-configured name.
"""把 Flexiv 当前工具(TCP + 负载)切换成 Studio 里配置好的名字。
Must run under RDK Python (rdk_py310). Robot must be IDLE; stop run_real.sh first
if ros2_control already owns the RDK connection.
必须在 RDK 的 Python(rdk_py310)下运行。机器人必须处于 IDLE;如果
ros2_control 已经占住了 RDK 连接,请先停掉 run_real.sh。
/home/cc/rdk_py310/bin/python ~/ros2/scripts/rdk_switch_tool.py --list
/home/cc/rdk_py310/bin/python ~/ros2/scripts/rdk_switch_tool.py --name Flange
@@ -17,7 +17,7 @@ import time
def _single_arm_groups(robot):
"""RDK 1.9 Tool API is single-arm (no JointGroup). Older builds use groups."""
"""RDK 1.9 的 Tool API 是单臂的(没有 JointGroup)。老版本用 groups。"""
info = robot.info()
groups = getattr(info, "single_arm_groups", None) or {}
if isinstance(groups, dict):
@@ -33,7 +33,7 @@ def _single_arm_groups(robot):
return [flexivrdk.JointGroup.ARM_1]
except Exception:
pass
return [None] # sentinel: call Tool.name()/Switch() without a group
return [None] # 哨兵:调用 Tool.name()/Switch() 时不带 group
def _tool_name(tool, group):
@@ -57,7 +57,7 @@ def _tool_switch(tool, group, target: str) -> None:
def _group_name(group) -> str:
if group is None:
return "arm"
return "单臂"
try:
import flexivrdk
@@ -91,7 +91,7 @@ def _ensure_idle(robot, mode) -> None:
time.sleep(0.25)
if not robot.operational():
raise RuntimeError(
"robot not operational — AUTO Remote + Enable, release E-stop"
"机器人未处于 operational 状态 —— 请在示教器切 AUTO Remote 并使能,松开急停"
)
robot.SwitchMode(mode.IDLE)
@@ -108,13 +108,13 @@ def _resolve_name(tool, want: str) -> str:
if hit:
return hit
raise SystemExit(
"tool %r not found in Studio pool.\n available: %s"
% (want, ", ".join(names) or "(empty)")
"工具 %r 不在 Studio 工具池里。\n 可选: %s"
% (want, ", ".join(names) or "(空)")
)
def _iface_ipv4s(name: str) -> list:
"""FastDDS whitelist wants IPv4 addresses, not NIC names like enp4s0."""
"""FastDDS 白名单要的是 IPv4 地址,而不是 enp4s0 这类网卡名。"""
import socket
import fcntl
import struct
@@ -147,7 +147,7 @@ def _iface_ipv4s(name: str) -> list:
addrs.append(cidr.split("/")[0])
except Exception:
pass
# unique, keep order
# 去重,保持原有顺序
seen = set()
uniq = []
for a in addrs:
@@ -170,7 +170,7 @@ def _rdk_whitelist(iface: str) -> list[list[str]]:
robot_lan = [a for a in _iface_ipv4s(iface) if a.startswith("192.168.2.")]
if robot_lan:
lists.append(robot_lan)
# unique
# 去重
seen: set[tuple[str, ...]] = set()
out: list[list[str]] = []
for item in lists:
@@ -185,7 +185,7 @@ def connect_rdk(sn: str, iface: str = ""):
import flexivrdk
for listen in _rdk_whitelist(iface):
print("RDK whitelist:", ", ".join(listen))
print("RDK 白名单:", ", ".join(listen))
for args in ((sn, listen), (sn, listen, True), (sn, listen, True, False)):
try:
return flexivrdk.Robot(*args)
@@ -194,51 +194,51 @@ def connect_rdk(sn: str, iface: str = ""):
except RuntimeError as exc:
msg = str(exc)
if "whitelist" in msg.lower() or "filtered out" in msg.lower():
print(" whitelist rejected, try next")
print(" 白名单被拒,试下一个")
break
if "Failed to connect" in msg or "failed to connect" in msg.lower():
print(" search timeout with this whitelist")
print(" 用这个白名单搜索超时")
break
raise
print("retry without iface filter")
print("不带 iface 过滤再试一次")
return flexivrdk.Robot(sn)
def main() -> int:
ap = argparse.ArgumentParser(description="Switch Flexiv Studio tool / TCP")
ap.add_argument("--sn", default="", help="robot serial (default: site.yaml)")
ap.add_argument("--iface", default="", help="NIC whitelist for RDK (e.g. enp4s0)")
ap = argparse.ArgumentParser(description="切换 Flexiv Studio 工具 / TCP")
ap.add_argument("--sn", default="", help="机械臂序列号(默认取 site.yaml)")
ap.add_argument("--iface", default="", help="RDK 用的网卡白名单(如 enp4s0)")
ap.add_argument(
"--name",
default="",
help="Studio tool name (Flange = no tool). Empty = site.yaml robot.tool_name",
help="Studio 工具名(Flange = 不装工具)。留空 = 用 site.yaml 的 robot.tool_name",
)
ap.add_argument("--list", action="store_true", help="list tools and exit")
ap.add_argument("--list", action="store_true", help="列出工具后退出")
args = ap.parse_args()
sn = (args.sn or "").strip()
want = (args.name or "").strip()
if not sn:
print("ERROR: --sn is required (run via run_real.sh / rdk_switch_tool.sh)", file=sys.stderr)
print("错误:必须提供 --sn(请通过 run_real.sh / rdk_switch_tool.sh 运行)", file=sys.stderr)
return 2
import flexivrdk
mode = flexivrdk.Mode
iface = (args.iface or "").strip()
print(f"=== RDK tool SN={sn} iface={iface or 'auto'} ===")
print(f"=== RDK 工具 SN={sn} iface={iface or 'auto'} ===")
robot = None
try:
robot = connect_rdk(sn, iface)
if robot.fault():
print("fault present, ClearFault ...")
print("检测到故障,正在 ClearFault ...")
if not robot.ClearFault():
print("ERROR: fault cannot be cleared", file=sys.stderr)
print("错误:故障无法清除", file=sys.stderr)
return 1
tool = flexivrdk.Tool(robot)
names = list(tool.list() or [])
groups = _single_arm_groups(robot)
print("Studio tools:", ", ".join(names) or "(none)")
print("Studio 工具:", ", ".join(names) or "(无)")
for g in groups:
cur = _tool_name(tool, g)
extra = ""
@@ -246,23 +246,23 @@ def main() -> int:
extra = " " + _fmt_params(_tool_params(tool, g))
except Exception:
pass
print(f" active [{_group_name(g)}]: {cur}{extra}")
print(f" 当前 [{_group_name(g)}]: {cur}{extra}")
if args.list:
return 0
if not want:
print("tool_name empty — keep current (set robot.tool_name in site.yaml to switch)")
print("RDK OK")
print("tool_name 为空 —— 保持当前工具(要切换请在 site.yaml 里设置 robot.tool_name)")
print("RDK 正常")
return 0
target = _resolve_name(tool, want)
already = all(_tool_name(tool, g) == target for g in groups)
if already:
print(f"already on [{target}]")
print("RDK OK")
print(f"已经处于 [{target}]")
print("RDK 正常")
return 0
print(f"switching → [{target}] (requires IDLE)")
print(f"正在切换到 → [{target}](需要 IDLE)")
_ensure_idle(robot, mode)
for g in groups:
_tool_switch(tool, g, target)
@@ -273,16 +273,16 @@ def main() -> int:
extra = " " + _fmt_params(_tool_params(tool, g))
except Exception:
pass
print(f" now [{_group_name(g)}]: {cur}{extra}")
print(f" 现在 [{_group_name(g)}]: {cur}{extra}")
if cur != target:
print(f"ERROR: switch failed, still {cur!r}", file=sys.stderr)
print(f"错误:切换失败,当前仍是 {cur!r}", file=sys.stderr)
return 1
print("RDK OK")
print("RDK 正常")
return 0
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
print(" Stop run_real.sh first (RDK is exclusive).", file=sys.stderr)
print(" Pendant: AUTO Remote + Enable Ethernet/RDK.", file=sys.stderr)
print(f"错误:{exc}", file=sys.stderr)
print(" 请先停掉 run_real.sh(RDK 是独占的)。", file=sys.stderr)
print(" 示教器:切 AUTO Remote,并使能 Ethernet/RDK。", file=sys.stderr)
return 1
finally:
robot = None
+4 -4
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# Switch / list Flexiv Studio tools via RDK (reads name from site.yaml).
# 通过 RDK 切换 / 列出 Flexiv Studio 工具(工具名从 site.yaml 读取)。
#
# bash ~/ros2/scripts/rdk_switch_tool.sh # switch to robot.tool_name
# bash ~/ros2/scripts/rdk_switch_tool.sh --list # print Studio tool pool
# bash ~/ros2/scripts/rdk_switch_tool.sh # 切换到 robot.tool_name
# bash ~/ros2/scripts/rdk_switch_tool.sh --list # 打印 Studio 工具池
# bash ~/ros2/scripts/rdk_switch_tool.sh --name Flange
#
# Stop run_real.sh first — RDK allows only one client.
# 先停掉 `run_real.sh` —— RDK 只允许一个客户端。
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+8 -8
View File
@@ -1,17 +1,17 @@
#!/usr/bin/env bash
# Terminal: D405 via official pyrealsense2 SDK — color + depth + stereo IR topics.
# With --preview the Qt window runs in a SEPARATE viewer process (realsense_view)
# so painting never slows the camera stream. Closing the window ≠ stop camera.
# Do NOT start view_realsense_depth.sh at the same time (USB is exclusive).
# 终端方式运行 D405:走官方 pyrealsense2 SDK —— 发布彩色 + 深度 + 双目 IR 话题。
# 带 --preview 时,Qt 窗口跑在**独立的**查看器进程(`realsense_view`)里,
# 这样绘制界面永远不会拖慢相机数据流。关掉窗口 ≠ 停止相机。
# 不要同时启动 `view_realsense_depth.sh`(USB 是独占的)。
#
# Usage:
# 用法:
# bash ~/ros2/scripts/run_camera_d405.sh
# bash ~/ros2/scripts/run_camera_d405.sh --preview
# bash ~/ros2/scripts/run_camera_d405.sh --arm 91 --preview
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PREVIEW=-1 # -1 = use site; 0/1 = force
PREVIEW=-1 # -1 = 沿用 site;0/1 = 强制
HIGH_FPS=0
# shellcheck disable=SC1091
@@ -143,8 +143,8 @@ fi
echo "=== next: bash ~/ros2/scripts/run_visual_servo_rt.sh --arm ${SITE_ROBOT_ARM:-89} [--control] ==="
if [[ "${DO_PREVIEW}" == "1" ]]; then
# Camera publishes only; the Qt viewer runs in its own process so painting
# can never starve the capture loop (shared GIL cost ~70 ms/frame before).
# 相机进程只负责发布;Qt 查看器跑在自己的进程里,所以界面绘制
# 永远不会把采集循环饿死(以前共用 GIL 时每帧要花约 70 ms)。
pkill -f 'lib/arm_application/realsense_view' 2>/dev/null || true
ros2 run arm_application realsense_color_pub "${CAM_ARGS[@]}" -p show_preview:=false &
CAM_PID=$!
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Overhead D435 between the two arms (eye-to-hand). Does not open either D405.
# 两臂之间的顶置 D435(`eye-to-hand`,相机固定在外)。不会打开任何一个 D405。
#
# bash ~/ros2/scripts/run_camera_d435.sh --preview
#
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# View URDF only (joint_state_publisher_gui + RViz)
# 只看 URDF(joint_state_publisher_gui + RViz)
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
+3 -3
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Both DH grippers (left PGEA + right RGI) as one ROS 2 node.
# 把两个 DH 夹爪(左 PGEA + 右 RGI)作为同一个 ROS 2 节点启动。
# bash ~/ros2/scripts/run_dual_gripper.sh
# Topics: /dual_gripper_driver/left|right/{command,status}
# Services: /dual_gripper_driver/left|right/execute (arm_msgs/srv/GripperExecute)
# 话题:/dual_gripper_driver/left|right/{command,status}
# 服务:/dual_gripper_driver/left|right/execute (arm_msgs/srv/GripperExecute)
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+6 -6
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# DH grippers on the IPC (Modbus RTU). Not Flexiv Studio / RDK.
# 工控机上的 DH 夹爪(Modbus RTU)。不是 Flexiv Studio / RDK 那一套。
#
# Left 89 PGEA parallel /dev/ttyUSB0
# Right 91 RGI rotary /dev/ttyUSB1
# 左臂 89 PGEA 平行夹爪 /dev/ttyUSB0
# 右臂 91 RGI 旋转夹爪 /dev/ttyUSB1
#
# bash ~/ros2/scripts/run_gripper.sh --arm 89 --cli probe
# bash ~/ros2/scripts/run_gripper.sh --arm 89 --cli init
@@ -13,9 +13,9 @@
# bash ~/ros2/scripts/run_gripper.sh --arm 91 --cli close
# bash ~/ros2/scripts/run_gripper.sh --arm 91 --cli rotate 90
# bash ~/ros2/scripts/run_gripper.sh --arm 91 --cli revolutions 2
# bash ~/ros2/scripts/run_gripper.sh --arm 89 # ROS node PGEA
# bash ~/ros2/scripts/run_gripper.sh --arm 91 # ROS node RGI
# bash ~/ros2/scripts/run_dual_gripper.sh # both ROS nodes
# bash ~/ros2/scripts/run_gripper.sh --arm 89 # ROS 节点 PGEA
# bash ~/ros2/scripts/run_gripper.sh --arm 91 # ROS 节点 RGI
# bash ~/ros2/scripts/run_dual_gripper.sh # 两个 ROS 节点都起
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+6 -6
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env bash
# Hand-eye calibration helpers.
# Scripts: ~/ros2/scripts/run_handeye.sh
# Config & results: ~/ros2/handeye/ ← 见 handeye/README.md
# 手眼标定辅助脚本。
# 脚本:~/ros2/scripts/run_handeye.sh
# 配置与结果:~/ros2/handeye/ ← 见 handeye/README.md
# poses/<臂号>_<模式>.yaml 采点(输入)
# results/<臂号>_<模式>.yaml 标定结果(输出)
# calibrate.yaml 板尺寸 / 运动 / 稳定性参数
# tmp/ 临时产物
# Site overrides: ~/ros2/config/site.yaml → handeye:
# 站点覆盖项:~/ros2/config/site.yaml → handeye:
#
# Usage:
# 用法:
# bash ~/ros2/scripts/run_handeye.sh calibrate
# bash ~/ros2/scripts/run_handeye.sh waypoints
# bash ~/ros2/scripts/run_handeye.sh teach # RDK 软拖拽(推荐)
@@ -61,7 +61,7 @@ source "${ROOT}/scripts/env.sh"
# shellcheck disable=SC1091
source "${ROOT}/scripts/lib/load_site_config.sh"
# Prefer package source tree (editable); fall back to install share
# 优先用包的源码树(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
+5 -5
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env bash
# Hole detection on the current-arm wrist D405. Detect only — no insert motion.
# 用当前臂腕部 D405 做孔检测。只检测 —— 不做插入动作。
#
# bash ~/ros2/scripts/run_camera_d405.sh --arm 89 --preview
# bash ~/ros2/scripts/run_hole_detect.sh --arm 89 --preview
#
# backend from site.yaml hole_detect.backend:
# two_stage YOLO workpiece bbox + HoughCircles (camera_test hole_detect.py)
# yolo YOLO boxes treated as holes
# hough full-image HoughCircles
# 后端取自 site.yaml 的 hole_detect.backend:
# two_stage YOLO 工件 bbox + HoughCircles(camera_test 的 hole_detect.py)
# yolo 把 YOLO 框直接当作孔
# hough 整图 HoughCircles
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+4 -4
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Real Rizon4 bringup — sets RDK runtime libs + launches stack backend:=real
# Note: no `set -u` — conda/RoboStack hooks reference unset vars (CONDA_BUILD, COLCON_TRACE)
# 真机 Rizon4 启动入口 —— 设置 RDK 运行时库,然后以 backend:=real 启动整栈
# 注意:这里不加 `set -u` —— conda/RoboStack 的钩子会引用未绑定的变量(CONDA_BUILD、COLCON_TRACE)
#
# bash ~/ros2/scripts/run_real.sh
# bash ~/ros2/scripts/run_real.sh --arm 91
@@ -31,10 +31,10 @@ source "${ROOT}/install/setup.bash"
# shellcheck disable=SC1091
source "${ROOT}/scripts/lib/load_site_config.sh"
# flexiv_hardware must load spdlog/RBDyn from flexiv_install (not conda fmt v11)
# `flexiv_hardware` 必须从 flexiv_install 加载 spdlog/RBDyn(不能用 conda 的 fmt v11)
export LD_LIBRARY_PATH="${FLEXIV_PREFIX}/lib:${LD_LIBRARY_PATH:-}"
# SITE_ROBOT_SN is the resolved Flexiv serial (89/91 aliases already mapped).
# `SITE_ROBOT_SN` 是解析后的 Flexiv 序列号(89/91 别名已经映射好了)。
ROBOT_SN="${SITE_ROBOT_SN:-${ROBOT_SN:-Rizon4-063989}}"
ROBOT_IP="${ROBOT_IP:-${SITE_ROBOT_IP:-192.168.100.1}}"
NET_IFACE="${SITE_NET_IFACE:-}"
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Quick-start: mock arm + RViz
# 快速启动:mock 机械臂 + RViz
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Orisys visuo-tactile (finger cameras, not D405). Config: site.yaml tactile:
# Orisys 视触觉(指端相机,不是 D405)。配置:site.yaml 的 tactile:
#
# bash ~/ros2/scripts/run_tactile.sh --cli
# bash ~/ros2/scripts/run_tactile.sh # ROS node /tactile/left|right/status
# bash ~/ros2/scripts/run_tactile.sh # ROS 节点 /tactile/left|right/status
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+6 -6
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env bash
# Real-time visual servo (reads ALL tunables from ~/ros2/config/site.yaml).
# 实时视觉伺服(所有可调参数全部从 ~/ros2/config/site.yaml 读取)。
#
# bash ~/ros2/scripts/run_visual_servo_rt.sh
# bash ~/ros2/scripts/run_visual_servo_rt.sh --control
# bash ~/ros2/scripts/run_visual_servo_rt.sh --arm 91 --control
#
# --control starts tracking from the current pose (no lift, no return-to-home).
# --control 从当前位姿直接开始跟踪(不抬升,也不回 home)。
# 89/91 手眼与相机各自独立:必须与 run_real.sh 同一 --arm。
#
# Switch YOLO weights: edit paths.yolo_weights in site.yaml
# 切换 YOLO 权重:改 site.yaml 里的 paths.yolo_weights
set -eo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -42,9 +42,9 @@ fi
if [[ "${CONTROL}" == true ]]; then
echo "=== waiting for rizon_arm_controller ACTIVE (T1: run_real.sh --arm ${SITE_ROBOT_ARM:-?}) ==="
# NOTE: action-list can show zombie entries from a crashed ros2_control,
# and `ros2 control` CLI is broken in this env (xmlrpc Fault). A raw
# service call only answers when the controller manager is actually alive.
# 注意:action-list 可能显示出崩溃过的 ros2_control 留下的僵尸条目,
# 而且这个环境里的 `ros2 control` CLI 是坏的(xmlrpc Fault)。直接发
# 服务请求只有控制器管理器真的活着时才会应答。
ready=false
for _i in $(seq 1 40); do
if timeout 5 ros2 service call /controller_manager/list_controllers \
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Show / validate the master site config.
# 显示 / 校验主站点配置。
# bash ~/ros2/scripts/show_site_config.sh
# SITE_CONFIG=/path/to/other.yaml bash ~/ros2/scripts/show_site_config.sh
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Smoke-test Flexiv RDK Python against site robot (no ROS / no GitHub).
# 针对站点机器人对 Flexiv RDK Python 做冒烟测试(不需要 ROS / 不访问 GitHub)。
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
+3 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Wire local Flexiv trees under ~/文档/Flexiv_Rizon4 into this workspace.
# Avoids re-cloning RDK / sim plugin from GitHub.
# 把 ~/文档/Flexiv_Rizon4 下的本地 Flexiv 代码树接入本工作区。
# 免得再从 GitHub 重新克隆 RDK / 仿真插件。
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -30,7 +30,7 @@ if [[ -d "${LOCAL_FLEXIV}/flexiv_sim_plugin" ]]; then
echo "Linked thirdparty/flexiv_sim_plugin -> ${LOCAL_FLEXIV}/flexiv_sim_plugin"
fi
# humble-v1 ROS hardware needs RDK 1.9.x (kept separately if present)
# humble-v1 的 ROS 硬件需要 RDK 1.9.x(如果存在,会单独保留一份)
if [[ -d "${SRC}/flexiv_rdk.bak.1787646725" ]]; then
ln -sfn "${SRC}/flexiv_rdk.bak.1787646725" "${SRC}/flexiv_rdk_v1"
touch "${SRC}/flexiv_rdk.bak.1787646725/COLCON_IGNORE"