Files
ros_contorl/scripts/lib/export_site_env.py
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

568 lines
26 KiB
Python
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 python3
"""读取 ``~/ros2/config/site.yaml``,打印 bash 的 export 行或 JSON。
用法:
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
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
try:
import yaml
except ImportError:
yaml = None # type: ignore
def _root() -> Path:
return Path(__file__).resolve().parents[2]
def site_path() -> Path:
env = os.environ.get("SITE_CONFIG", "").strip()
if env:
return Path(env).expanduser()
return _root() / "config" / "site.yaml"
def load_site(path: Path | None = None) -> dict:
path = path or site_path()
if not path.is_file():
raise FileNotFoundError(f"找不到 site 配置: {path}")
text = path.read_text(encoding="utf-8")
if yaml is None:
raise RuntimeError("需要 PyYAML:pip/conda install pyyaml")
data = yaml.safe_load(text) or {}
if not isinstance(data, dict):
raise ValueError(f"无效的 site.yaml: {path}")
return data
def list_robots(data: dict) -> list[dict]:
"""从 robot.robots 取清单条目(列表或按 SN 索引的映射)。"""
raw = (data.get("robot") or {}).get("robots") or []
out: list[dict] = []
if isinstance(raw, dict):
for sn, item in raw.items():
row = dict(item or {}) if isinstance(item, dict) else {}
row.setdefault("sn", str(sn))
out.append(row)
return out
if isinstance(raw, list):
for item in raw:
if isinstance(item, dict) and str(item.get("sn") or "").strip():
out.append(dict(item))
return out
# --arm 89|91|left|right 与 SN 别名。禁止把 89 的手眼/相机套到 91。
_ARM_ALIASES = {
"89": "Rizon4-063989",
"left": "Rizon4-063989",
"左": "Rizon4-063989",
"左臂": "Rizon4-063989",
"左臂89": "Rizon4-063989",
"91": "Rizon4-063991",
"right": "Rizon4-063991",
"右": "Rizon4-063991",
"右臂": "Rizon4-063991",
"右臂91": "Rizon4-063991",
}
_ARM_SECTION_KEYS = ("camera", "gripper", "paths", "servo", "handeye", "go_home", "yolo")
def _deep_merge(base: dict, overlay: dict) -> dict:
out = dict(base)
for k, v in overlay.items():
if isinstance(out.get(k), dict) and isinstance(v, dict):
out[k] = _deep_merge(out[k], v)
else:
out[k] = v
return out
def normalize_robot_sn(data: dict, want: str) -> str:
"""把 89/91/left/right/label 映射成完整的 Flexiv SN。"""
w = (want or "").strip()
if not w:
return w
mapped = _ARM_ALIASES.get(w) or _ARM_ALIASES.get(w.lower())
if mapped:
return mapped
for item in list_robots(data):
sn = str(item.get("sn") or "").strip()
label = str(item.get("label") or "").strip()
arm = str(item.get("arm") or "").strip().lower()
if w == sn or w == label or w.lower() == arm:
return sn
if sn.endswith(w) and w.isdigit() and len(w) >= 2:
return sn
return w
def resolve_robot(data: dict, sn: str | None = None) -> dict:
"""当前生效的机械臂:环境变量 ``ROBOT_SN`` 覆盖 robot.sn;清单补齐 ip/iface/frames。"""
robot = dict(data.get("robot") or {})
catalog = list_robots(data)
want = normalize_robot_sn(
data,
sn or os.environ.get("ROBOT_SN") or os.environ.get("ROBOT_ARM") or robot.get("sn") or "",
)
match: dict = {}
for item in catalog:
if str(item.get("sn") or "").strip() == want:
match = dict(item)
break
out = {k: v for k, v in robot.items() if k != "robots"}
out.update({k: v for k, v in match.items() if k != "robots"})
out["sn"] = want
if not str(out.get("arm") or "").strip():
if "063989" in want:
out["arm"] = "left"
elif "063991" in want:
out["arm"] = "right"
if not str(out.get("joint_prefix") or "").strip() and want:
out["joint_prefix"] = f"{want}_"
if not str(out.get("base_frame") or "").strip() and want:
out["base_frame"] = f"{want}_base_link"
if not str(out.get("ee_frame") or "").strip() and want:
out["ee_frame"] = f"{want}_flange"
net = data.get("network") or {}
if not str(out.get("iface") or "").strip():
out["iface"] = str(net.get("prefer_iface") or "")
if not str(out.get("ip") or "").strip():
out["ip"] = "192.168.100.1"
if not str(out.get("pc_ip") or "").strip():
out["pc_ip"] = "192.168.100.10"
return out
def apply_active_arm(data: dict, sn: str | None = None) -> tuple[dict, dict]:
"""把 robots[] 里的 camera/gripper/paths/servo 合并到当前 SN 对应的 site 默认值上。"""
robot = resolve_robot(data, sn)
merged = dict(data)
for key in _ARM_SECTION_KEYS:
base = merged.get(key) if isinstance(merged.get(key), dict) else {}
ov = robot.get(key) if isinstance(robot.get(key), dict) else {}
merged[key] = _deep_merge(dict(base or {}), dict(ov or {}))
return merged, robot
def camera_ns(data: dict) -> str:
"""当前腕部相机(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]:
"""当前机械臂的(命名空间, 光学坐标系, 深度话题)。"""
ns = camera_ns(data)
yolo = data.get("yolo") or {}
frame = str(yolo.get("camera_frame") or "").strip()
depth = str(yolo.get("depth_topic") or "").strip()
if frame in ("", "d405_color_optical_frame"):
frame = f"{ns}_color_optical_frame"
if depth in ("", "/d405/aligned_depth_to_color/image_raw"):
depth = f"/{ns}/aligned_depth_to_color/image_raw"
return ns, frame, depth
def flatten(data: dict) -> dict:
"""供 bash 脚本使用的扁平 ``SITE_*`` 环境变量映射。"""
data, robot = apply_active_arm(data)
paths = data.get("paths") or {}
cam = data.get("camera") or {}
yolo = data.get("yolo") or {}
servo = data.get("servo") or {}
home = data.get("go_home") or {}
net = data.get("network") or {}
he = data.get("handeye") or {}
grip = data.get("gripper") or {}
named_poses = str(
robot.get("named_poses_file")
or paths.get("named_poses_file")
or str(_root() / "src" / "arm_control" / "config" / "named_poses.yaml")
)
weights = str(paths.get("yolo_weights") or "")
# 解析符号链接,方便日志里看到真实文件(历史上有人用 active.pt 做切换,已废弃)
try:
weights_resolved = str(Path(weights).expanduser().resolve())
except Exception:
weights_resolved = weights
out = {
"SITE_CONFIG": str(site_path()),
"SITE_ROS2_ROOT": str(_root()),
"SITE_ROBOT_SN": str(robot.get("sn") or "Rizon4-063989"),
"SITE_ROBOT_IP": str(robot.get("ip") or "192.168.100.1"),
"SITE_PC_IP": str(robot.get("pc_ip") or "192.168.100.10"),
"SITE_JOINT_PREFIX": str(robot.get("joint_prefix") or ""),
"SITE_BASE_FRAME": str(robot.get("base_frame") or ""),
"SITE_EE_FRAME": str(robot.get("ee_frame") or ""),
"SITE_ROBOT_TOOL_NAME": str(robot.get("tool_name") or ""),
"SITE_ROBOT_LABEL": str(robot.get("label") or ""),
"SITE_ROBOT_ARM": str(robot.get("arm") or ""),
"SITE_NAMED_POSES_FILE": named_poses,
"SITE_ROBOT_CATALOG": ",".join(
str(r.get("sn") or "") for r in list_robots(data) if r.get("sn")
),
"SITE_YOLO_WEIGHTS": weights,
"SITE_YOLO_WEIGHTS_RESOLVED": weights_resolved,
"SITE_HANDEYE_WAYPOINTS": str(
paths.get("handeye_waypoints")
or str(_root() / "handeye" / "poses" / "89_eye_in_hand.yaml")
),
"SITE_HANDEYE_CALIBRATE": str(
paths.get("handeye_calibrate")
or str(_root() / "handeye" / "calibrate.yaml")
),
"SITE_HANDEYE_TOUR": str(
paths.get("handeye_tour")
or str(_root() / "handeye" / "waypoints.yaml")
),
"SITE_HANDEYE_WORKDIR": str(
paths.get("handeye_workdir") or str(_root() / "handeye" / "tmp")
),
"SITE_HANDEYE_FILE": str(
paths.get("handeye_file")
or str(_root() / "handeye" / "results" / "89_eye_in_hand.yaml")
),
"SITE_HANDEYE_MODE": str(he.get("mode") or "eye_in_hand"),
# 板尺寸默认值必须与 site.yaml / 结果文件头里的实际板子一致。
# 这里原先是 11/16/0.011(一张早期用过、现已不存在的板),与 site.yaml 的
# 8/11/0.015 冲突:一旦 site.yaml 缺这几个键就会静默用错尺寸,
# 表现为"标定能跑完但结果明显不对"。
"SITE_HANDEYE_BOARD_COLS": str(int(he.get("board_cols") or 8)),
"SITE_HANDEYE_BOARD_ROWS": str(int(he.get("board_rows") or 11)),
"SITE_HANDEYE_SQUARE_M": str(float(he.get("square_size_m") or 0.015)),
"SITE_HANDEYE_VEL_SCALE": str(float(he.get("velocity_scale") or 0.05)),
"SITE_HANDEYE_MOVE_T": str(float(he.get("move_time_sec") or 8.0)),
"SITE_HANDEYE_SETTLE_SEC": str(float(he.get("settle_sec") or 1.5)),
"SITE_HANDEYE_SETTLE_QD": str(float(he.get("settle_qd_max") or 0.015)),
"SITE_HANDEYE_SETTLE_STABLE": str(float(he.get("settle_stable_sec") or 0.6)),
"SITE_HANDEYE_SETTLE_TO": str(float(he.get("settle_timeout_sec") or 8.0)),
"SITE_HANDEYE_PIN_HOLDS": str(int(he.get("pin_holds") if he.get("pin_holds") is not None else 2)),
"SITE_HANDEYE_PIN_SEC": str(float(he.get("pin_hold_sec") or 0.45)),
"SITE_HANDEYE_SKIP_UNSTABLE": (
"true" if he.get("skip_unstable_poses", True) else "false"
),
"SITE_FASTDDS_PROFILE": str(paths.get("fastdds_profile") or ""),
"SITE_CAM_NAME": camera_ns(data),
"SITE_CAM_SERIAL": str(cam.get("serial") or ""),
"SITE_CAM_WIDTH": str(int(cam.get("width") or 848)),
"SITE_CAM_HEIGHT": str(int(cam.get("height") or 480)),
"SITE_CAM_FPS": str(int(cam.get("fps") or 30)),
"SITE_CAM_ENABLE_DEPTH": "1" if cam.get("enable_depth", True) else "0",
"SITE_CAM_ENABLE_INFRA": "1" if cam.get("enable_infra", True) else "0",
"SITE_CAM_PREVIEW": "1" if cam.get("preview", True) else "0",
"SITE_CAM_PREVIEW_STEREO": "1" if cam.get("preview_stereo", True) else "0",
"SITE_CAM_OVERLAY_HOLD": str(float(cam.get("overlay_hold_sec") or 1.5)),
"SITE_CAM_USE_SITE_RES": "1" if cam.get("use_site_resolution", True) else "0",
"SITE_CAM_AUTO_EXPOSURE": "1" if cam.get("auto_exposure", True) else "0",
"SITE_CAM_AE_LIMIT_US": str(float(cam.get("ae_limit_us") if cam.get("ae_limit_us") is not None else 4000)),
"SITE_CAM_EMITTER": "1" if cam.get("emitter_enabled", False) else "0",
"SITE_CAM_LASER_POWER": str(float(cam.get("laser_power") if cam.get("laser_power") is not None else 0)),
"SITE_CAM_EXPOSURE_US": str(float(cam.get("exposure_us") or 0)),
"SITE_CAM_GAIN": str(float(cam.get("gain") or 0)),
"SITE_D435_SERIAL": str(((data.get("cameras") or {}).get("d435") or {}).get("serial") or ""),
"SITE_D435_WIDTH": str(int(((data.get("cameras") or {}).get("d435") or {}).get("width") or 1280)),
"SITE_D435_HEIGHT": str(int(((data.get("cameras") or {}).get("d435") or {}).get("height") or 720)),
"SITE_D435_FPS": str(int(((data.get("cameras") or {}).get("d435") or {}).get("fps") or 30)),
"SITE_D405_LEFT_SERIAL": str(((data.get("cameras") or {}).get("d405_left") or {}).get("serial") or "352122274408"),
"SITE_D405_RIGHT_SERIAL": str(((data.get("cameras") or {}).get("d405_right") or {}).get("serial") or "260522274442"),
"SITE_YOLO_CONF": str(yolo.get("conf") or 0.10),
"SITE_YOLO_IMGSZ": str(int(yolo.get("imgsz") or 640)),
"SITE_YOLO_DEVICE": str(yolo.get("device") or "0"),
"SITE_YOLO_MIN_CONF": str(yolo.get("min_conf") or 0.12),
"SITE_GO_HOME_POSE": str(home.get("pose") or "home"),
"SITE_GO_HOME_SPEED": str(home.get("speed") or 0.25),
"SITE_NET_IFACE": str(robot.get("iface") or net.get("prefer_iface") or ""),
"SITE_SERVO_Z_REF": str(servo.get("z_ref_m") or 0.40),
"SITE_EE_X_M": str(float(servo.get("ee_x_m") or 0.25)),
"SITE_EE_Y_M": str(float(servo.get("ee_y_m") or 0.0)),
"SITE_EE_Z_M": str(float(servo.get("ee_z_m") or 0.24)),
"SITE_SERVO_WATCHDOG": str(servo.get("watchdog_timeout_sec") or 0.3),
"SITE_CAMERA_FRAME": camera_frames(data)[1],
"SITE_GRIP_VENDOR": str(grip.get("vendor") or "dh"),
"SITE_GRIP_MODEL": str(grip.get("model") or "PGEA-100-20-W"),
"SITE_GRIP_SN": str(grip.get("sn") or ""),
"SITE_GRIP_BACKEND": str(grip.get("backend") or "usb"),
"SITE_GRIP_DEVICE": str(grip.get("device_name") or "GripperDahuanModbus"),
"SITE_GRIP_FORCE_N": str(float(grip.get("force_n") or 50.0)),
"SITE_GRIP_VEL_MPS": str(float(grip.get("vel_mps") or 0.05)),
"SITE_GRIP_PORT": str(grip.get("port") or "/dev/ttyUSB0"),
"SITE_GRIP_BAUD": str(int(grip.get("baud") or 115200)),
"SITE_GRIP_SLAVE": str(int(grip.get("slave_id") or 1)),
"SITE_GRIP_STROKE_MM": str(float(grip.get("stroke_mm") or 20.0)),
"SITE_GRIP_FORCE": str(int(grip.get("force_pct") or 50)),
"SITE_GRIP_SPEED": str(int(grip.get("speed_pct") or 50)),
"SITE_GRIP_AUTO_INIT": "true" if grip.get("auto_init", True) else "false",
"SITE_GRIP_RS485_RTS": str(grip.get("rs485_rts") or ""),
"SITE_HOLE_BACKEND": str((data.get("hole_detect") or {}).get("backend") or "two_stage"),
"SITE_HOLE_WEIGHTS": str(
(data.get("hole_detect") or {}).get("weights")
or paths.get("hole_weights")
or str(_root() / "models" / "hole_insert" / "detect_best_1.onnx")
),
"SITE_HOLE_WORKPIECE_WEIGHTS": str(
(data.get("hole_detect") or {}).get("workpiece_weights")
or paths.get("hole_workpiece_weights")
or weights
),
"SITE_HOLE_CONFIG": str(
(data.get("hole_detect") or {}).get("config")
or (
str(_root() / "src" / "arm_application" / "config" / "hole_detect.yaml")
if str((data.get("hole_detect") or {}).get("backend") or "two_stage")
in ("two_stage", "hough_roi")
else str(_root() / "src" / "arm_application" / "config" / "hole_insert.yaml")
)
),
"SITE_TACTILE_BACKEND": str((data.get("tactile") or {}).get("backend") or "orisys"),
"SITE_TACTILE_CONFIG": str((data.get("tactile") or {}).get("config") or "dd03"),
"SITE_TACTILE_CONFIG_FILE": str(
(data.get("tactile") or {}).get("config_file")
or str(_root() / "src" / "arm_application" / "config" / "orisys" / "dd03.json")
),
"SITE_TACTILE_LEFT": str((data.get("tactile") or {}).get("left_video", 0)),
"SITE_TACTILE_RIGHT": str((data.get("tactile") or {}).get("right_video", 2)),
}
return out
def gripper_overrides(data: dict) -> dict:
"""供 dh_pgea_node 使用的 ROS 参数字典(DH Modbus,不是 Flexiv 夹爪)。"""
data, _robot = apply_active_arm(data)
g = data.get("gripper") or {}
return {
"port": str(g.get("port") or "/dev/ttyUSB0"),
"baud": int(g.get("baud") or 115200),
"slave_id": int(g.get("slave_id") or 1),
"stroke_mm": float(g.get("stroke_mm") or 20.0),
"force_pct": int(g.get("force_pct") or 50),
"speed_pct": int(g.get("speed_pct") or 50),
"auto_init": bool(g.get("auto_init", True)),
"state_hz": float(g.get("state_hz") or 20.0),
"model": str(g.get("model") or "PGEA-100-20-W"),
"serial_number": str(g.get("sn") or ""),
"open_position": int(g.get("open_position") if g.get("open_position") is not None else 1000),
"close_position": int(g.get("close_position") if g.get("close_position") is not None else 0),
"rs485_rts": str(g.get("rs485_rts") or ""),
}
def perception_overrides(data: dict) -> dict:
"""供 perception_node 使用的 ROS 参数字典。"""
data, _robot = apply_active_arm(data)
paths = data.get("paths") or {}
yolo = data.get("yolo") or {}
cam_ns, cam_frame, depth_topic = camera_frames(data)
obj = str(yolo.get("object_frame") or "").strip()
if not obj or obj == "tracked_object":
arm = str((_robot or {}).get("arm") or "")
sn = str((_robot or {}).get("sn") or "")
if "063991" in sn or arm == "right":
obj = "tracked_object_91"
elif "063989" in sn or arm == "left":
obj = "tracked_object_89"
else:
obj = "tracked_object"
return {
"weights": str(paths.get("yolo_weights") or ""),
"conf": float(yolo.get("conf") or 0.10),
"iou": float(yolo.get("iou") or 0.45),
"imgsz": int(yolo.get("imgsz") or 640),
"device": str(yolo.get("device") or "0"),
"half_precision": bool(yolo.get("half_precision", True)),
"backend": str(yolo.get("backend") or "auto"),
"mp_infer": bool(yolo.get("mp_infer", True)),
"min_conf": float(yolo.get("min_conf") or 0.12),
"vision_hz": float(yolo.get("vision_hz") or 30.0),
"infer_max_width": int(yolo.get("infer_max_width") if yolo.get("infer_max_width") is not None else 0),
"box_ema_alpha": float(yolo.get("box_ema_alpha") or 0.85),
"miss_clear_frames": int(yolo.get("miss_clear_frames") or 12),
"overlay_hold_on_miss": bool(yolo.get("overlay_hold_on_miss", True)),
"jump_px": float(yolo.get("jump_px") or 280.0),
"jump_z_m": float(yolo.get("jump_z_m") or 0.20),
"z_ref_m": float(yolo.get("z_ref_m") or 0.22),
"min_depth_m": float(yolo.get("min_depth_m") or 0.04),
"max_depth_m": float(yolo.get("max_depth_m") or 0.55),
"work_z_max_m": float(yolo.get("work_z_max_m") or 0.50),
"hold_point_on_miss": bool(yolo.get("hold_point_on_miss", True)),
"miss_hold_point_frames": int(yolo.get("miss_hold_point_frames") or 2),
"meas_blend": float(yolo.get("meas_blend") or 0.65),
"z_alpha_min": float(yolo.get("z_alpha_min") or 0.10),
"z_alpha_max": float(yolo.get("z_alpha_max") or 0.60),
"z_alpha_gain": float(yolo.get("z_alpha_gain") or 2.0),
"camera_frame": cam_frame,
"object_frame": obj,
"depth_topic": depth_topic,
"camera_source": cam_ns,
"feature_mode": str(yolo.get("feature_mode") or "plane"),
"plane_inlier_m": float(yolo.get("plane_inlier_m") or 0.008),
"plane_band_m": float(yolo.get("plane_band_m") or 0.06),
}
def servo_overrides(data: dict) -> dict:
"""供 servo_node 使用的 ROS 参数字典。"""
data, robot = apply_active_arm(data)
paths = data.get("paths") or {}
yolo = data.get("yolo") or {}
servo = data.get("servo") or {}
_ns, cam_frame, _depth = camera_frames(data)
out = {
"joint_prefix": str(robot.get("joint_prefix") or ""),
"base_frame": str(robot.get("base_frame") or ""),
"ee_frame": str(robot.get("ee_frame") or ""),
"handeye_file": str(paths.get("handeye_file") or ""),
"camera_frame": cam_frame,
"handeye_child_frame": cam_frame,
"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),
}
# 透传其余与节点参数同名的 servo 键
keys = [
"control_hz", "watchdog_timeout_sec", "lost_timeout_sec", "stale_hold_sec",
"max_extrapolate_sec", "predict_enable", "predict_sec",
"predict_v_max", "predict_jump_m",
"kp_xy", "ki_xy", "kd_xy", "kp_z", "ki_z", "kd_z", "i_sep_pos_m",
"deadzone_m", "deadzone_z_m", "v_max_xy", "v_max_z", "v_approach_z",
"z_min_m", "approach_xy_gate_m", "min_ee_z_m", "e_xy_clip_m",
"ee_x_m", "ee_y_m", "ee_z_m", "ee_hold_xy", "ee_vertical_down",
"xy_track", "hover_offset_ee_xy_m", "arm_label",
"ee_hold_kp", "ee_w_max",
"lock_hold_sec",
"a_max", "max_qdot", "damping",
"vel_lpf_alpha", "dq_window_size", "traj_stream_hz", "exec_horizon_sec",
"min_cmd_delta_rad", "point_topic", "traj_topic",
"use_static_handeye_tf",
"workspace_xyz_min", "workspace_xyz_max",
]
for k in keys:
if k in servo:
out[k] = servo[k]
return out
def main() -> int:
ap = argparse.ArgumentParser()
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|完整 SN")
args = ap.parse_args()
if args.sn:
os.environ["ROBOT_SN"] = str(args.sn)
path = Path(args.config).expanduser() if args.config else site_path()
data = load_site(path)
flat = flatten(data)
if args.json:
print(json.dumps({"site": data, "env": flat}, ensure_ascii=False, indent=2))
return 0
if args.do_print:
print(f"site 配置: {path}")
print(
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" 清单 : {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" 警告:robot.sn {flat['SITE_ROBOT_SN']!r} 不在 robots[] 里",
file=sys.stderr,
)
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(
" 标定板 : "
f"{flat['SITE_HANDEYE_BOARD_COLS']}x{flat['SITE_HANDEYE_BOARD_ROWS']} "
f"内角点, square={flat['SITE_HANDEYE_SQUARE_M']} m"
)
print(
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 "
f"gain={flat.get('SITE_CAM_GAIN')} "
f"ae_limit={flat.get('SITE_CAM_AE_LIMIT_US')}us emitter={flat.get('SITE_CAM_EMITTER')}"
)
print(
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" 夹爪 : {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" 触觉 : 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" 警告:手眼文件缺失: {he_path}", file=sys.stderr)
else:
try:
he_data = yaml.safe_load(he_path.read_text(encoding="utf-8")) or {}
except Exception:
he_data = {}
if isinstance(he_data, dict) and (
he_data.get("uncalibrated") or "T_4x4" not in he_data
):
print(
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(
" 警告:91 用的是左腕 D405 的序列号 — 请检查 robots[].camera.serial",
file=sys.stderr,
)
if "RGI" in str(flat.get("SITE_GRIP_MODEL") or ""):
print(
" 提示:本臂是 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" 警告:权重文件缺失: {w}", file=sys.stderr)
return 2
return 0
# bash 导出
for k, v in flat.items():
# 为 eval 转义
vv = str(v).replace("'", "'\"'\"'")
print(f"export {k}='{vv}'")
return 0
if __name__ == "__main__":
raise SystemExit(main())