Files
ros_contorl/scripts/lib/real_geometry_check.py
T
arm_dev 98f4277849 真机开工体检四件套:USB 链路 / V4L 设备 / RDK 状态 / 手眼几何
这四个都是「现场看着正常、结果不对」的故障源,共同特征是**不报错**,所以要把它们
从流程里拎出来单独判,并且给出可执行的下一步,而不是只报一个失败。

scripts/lib/usb_health.py
  RealSense 协商到 480Mbps(USB 2.0)时能出图、不报错,只是帧率掉到 1/2 且
  深度不稳定。这里解析 sysfs 拓扑,把「速率对不对」独立于「有没有图」来判,
  并在 --watch 下统计掉线/重连次数(线/口/供电 Hub 的问题会表现为反复抖)。

scripts/lib/v4l_health.py
  触觉相机有两类完全不同的坏法,处置方式相反,以前都只表现为「没有 /dev/video*」:
    · unplugged —— sysfs 里也没有,是没插/线坏
    · wedged    —— sysfs 里在、/dev/video* 没有,是 USB 端点了卡死(要复位或重插)
  2026-09-28 现场左指就是 wedged(1-3.4.1 在 uvcvideo 下,video 节点缺失)。

scripts/lib/rdk_status.py
  ros2_control 卡在 wait_for_operational 时,现象是「栈起来了但关节全是 0、
  流程一直等」。原因全在 OperationalStatus 上,而它只有 RDK 读得到。
  这里把状态翻成人话 + 处置(Auto(Remote) / 松急停 / 清故障)。
  判据取自厂商头文件原话:IN_MANUAL_MODE = "need to switch to Auto (Remote) mode"。

scripts/lib/real_geometry_check.py
  手眼矩阵与悬停几何。手眼写坏不会报错,只会让位姿整体偏几毫米到几厘米;
  判据:R 正交(|RᵀR−I| ≤ 2e-3)、det(R)=+1、平移量级在 10–500mm。
  悬停几何直接复用 gauge_fsm.hover_z_guard(流程里唯一判定处,不另写一套);
  工件面未标定(site.yaml 写 0.0 = 未知)时**明确报 FAIL 并说明判不了**,
  而不是按 0.0 算出一个假的「已修正」结论。

测试:test_usb_health / test_v4l_health / test_real_geometry_check 共 33 例,
全部钉在「判据要能报错」上(故意弄坏的输入必须被抓到)。

顺带 .gitignore 补 log_real/:真机各终端日志是运行产物,与 log_sim/ 对称。
2026-09-28 10:17:32 +08:00

203 lines
8.5 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
"""开工前的**几何自检**:手眼标定矩阵 + 悬停高度会不会把工具捅进工件。
这两件事都属于"配置/文件看着没问题,但一跑就把工件推走"的那一类,而且
**征兆出现在别的环节**(S05 残差恒不收敛、看着像检测坏了)。所以放在开工前
用几何查一遍,比在流程里翻日志便宜得多。
1) 手眼矩阵自洽:``handeye/results/<SN>_eye_in_hand.yaml`` 的 ``T_4x4``
必须是刚体变换 —— R 正交、det=+1、平移量级合理(几厘米到几十厘米)。
标定中断/写坏了半个文件时,矩阵会是"差不多但不正交"的,用它算出来的位姿
会整体偏几毫米到几厘米,而**不会**报任何错。
2) 悬停高度 vs 工具长:判据不在这里重写 —— 直接调用 ``hover_z_guard``
(``arm_application.gauge_fsm``),那是流程里唯一的判定处。
2026-09-24 的教训:悬停高按**相机**视距定,而工具比相机长 ⇒ 悬停时规尖
已经钻进工件 25mm,S05 横向对准把工件拖走 92mm。这里提前喊出来。
"""
from __future__ import annotations
import argparse
import json
import math
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence
ROOT = Path(__file__).resolve().parents[2]
SCRIPTS_LIB = ROOT / "scripts" / "lib"
# 平移量级合理区间 [m]:腕部相机 eye-in-hand 的平移是"法兰到相机"的距离。
T_MIN_M = 0.01
T_MAX_M = 0.50
# 正交性容差:R^T R 与 I 的每个元素偏差上限
ORTHO_TOL = 2e-3
def _matmul3(a: Sequence[Sequence[float]], b: Sequence[Sequence[float]]) -> List[List[float]]:
return [
[sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)] for i in range(3)
]
def check_transform(T: Any) -> List[str]:
"""检查 4x4 齐次矩阵是不是一个像样的刚体变换。返回问题列表(空=没问题)。"""
problems: List[str] = []
try:
rows = [list(map(float, r)) for r in T]
except (TypeError, ValueError) as exc:
return [f"T_4x4 里有非数字:{exc}"]
if len(rows) != 4 or any(len(r) != 4 for r in rows):
return [f"T_4x4 必须是 4x4,实际 {len(rows)}x{len(rows[0]) if rows else 0}"]
if any(abs(rows[3][i] - v) > 1e-9 for i, v in enumerate((0.0, 0.0, 0.0, 1.0))):
problems.append(f"最后一行应该是 [0,0,0,1],实际 {rows[3]}")
R = [r[:3] for r in rows[:3]]
# R^T R 应该等于 I
Rt = [[R[j][i] for j in range(3)] for i in range(3)]
RtR = _matmul3(Rt, R)
worst = max(
abs(RtR[i][j] - (1.0 if i == j else 0.0)) for i in range(3) for j in range(3)
)
if worst > ORTHO_TOL:
problems.append(
f"旋转部分不正交(|RᵀR−I| 最大 {worst:.2e} > {ORTHO_TOL:.0e})"
"—— 标定文件写坏了或标定没收敛;这种矩阵不会报错,只会让位姿整体偏"
)
det = (
R[0][0] * (R[1][1] * R[2][2] - R[1][2] * R[2][1])
- R[0][1] * (R[1][0] * R[2][2] - R[1][2] * R[2][0])
+ R[0][2] * (R[1][0] * R[2][1] - R[1][1] * R[2][0])
)
if abs(det - 1.0) > 1e-3:
problems.append(f"det(R)={det:.4f},应该是 +1(−1 表示镜像/坐标系搞反了)")
t = [rows[i][3] for i in range(3)]
norm = math.sqrt(sum(v * v for v in t))
if not (T_MIN_M <= norm <= T_MAX_M):
problems.append(
f"平移量级 {norm * 1000:.1f}mm 不在合理区间 "
f"[{T_MIN_M * 1000:.0f}, {T_MAX_M * 1000:.0f}]mm —— "
"要么单位不是米,要么标定结果跑飞了"
)
return problems
def load_T(path: Path) -> Any:
import yaml
doc = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if "T_4x4" not in doc:
raise KeyError(f"{path} 里没有 T_4x4(键:{sorted(doc)})")
return doc["T_4x4"]
def geometry_report(site_path: Path, sn: str) -> Dict[str, Any]:
"""跑一遍两项几何检查,返回结构化结论。"""
sys.path.insert(0, str(SCRIPTS_LIB))
import export_site_env as ex
sys.path.insert(0, str(ROOT / "install" / "arm_application" / "lib" / "python3.11" / "site-packages"))
from arm_application.gauge_config import from_site
from arm_application.gauge_fsm import hover_z_guard
data = ex.load_site(site_path)
merged, _robot = ex.apply_active_arm(data, sn)
cfg = from_site(merged)
out: Dict[str, Any] = {"handeye": {}, "hover": {}}
he_file = Path(str((merged.get("paths") or {}).get("handeye_file") or ""))
out["handeye"]["file"] = str(he_file)
if not he_file.is_file():
out["handeye"]["problems"] = [f"手眼文件不存在:{he_file}"]
else:
try:
T = load_T(he_file)
out["handeye"]["translation_m"] = [T[i][3] for i in range(3)]
out["handeye"]["problems"] = check_transform(T)
except Exception as exc: # noqa: BLE001
out["handeye"]["problems"] = [f"读手眼文件失败:{exc}"]
servo = merged.get("servo") or {}
stage2 = merged.get("stage2") or {}
hover = servo.get("hole_hover_z_m")
hover = float(hover) if hover is not None else float(stage2.get("hover_z_m") or 0.30)
gap = float(stage2.get("final_gap_mm") or 1.5) / 1000.0
out["hover"] = {
"hole_hover_z_m": hover,
"tool_tip_below_flange_m": cfg.tool_tip_below_flange_m,
"workpiece_surface_z_m": cfg.workpiece_surface_z_m,
"final_gap_m": gap,
"suggested_z_m": hover,
"problems": [],
"uncalibrated_surface": cfg.workpiece_surface_z_m <= 0.0,
}
if out["hover"]["uncalibrated_surface"]:
# site.yaml 用 0.0 表示"还不知道"(见 gauge_config 的默认值注释)。这时
# hover_z_guard 拿不到真正的工件面,算出来的结论是**假的** —— 与其报一个
# 假的 WARN,不如直接说"这一项现在判不了,得先标定"。
out["hover"]["problems"] = [
"工件上表面 z 没标定(site.yaml 写 0.0 = 未知)—— 悬停高度**没有**可靠的"
"几何依据。判据要把「工具长」和「工件面」比,缺一个就算不出来;"
"而 S05 的横向对准正好是在悬停高度上做的:悬停时工具尖端低于工件面就会把"
"工件推走(2026-09-24 实测推走 92mm,之后残差恒 13mm 永不收敛,"
"看着像检测坏了)。"
"修法:跑一次二阶段定位(bash scripts/run_stage2_locate.sh --arm 89),"
"它会把实测的工件面 z 打出来,把这个值填回 "
"robots[<sn>].gauge_assembly.workpiece_surface_z_m,再重启流程。"
]
else:
_fixed, why = hover_z_guard(
hover, cfg.workpiece_surface_z_m, cfg.tool_tip_below_flange_m, gap
)
out["hover"]["suggested_z_m"] = _fixed
out["hover"]["problems"] = [why] if why else []
return out
def main(argv: Optional[Sequence[str]] = None) -> int:
ap = argparse.ArgumentParser(description="开工前几何自检(手眼 + 悬停高度)")
ap.add_argument("--sn", default="")
ap.add_argument("--site", default=str(ROOT / "config" / "site.yaml"))
ap.add_argument("--json", action="store_true")
args = ap.parse_args(argv)
rep = geometry_report(Path(args.site), args.sn)
if args.json:
print(json.dumps(rep, ensure_ascii=False, indent=2))
problems: List[str] = []
he = rep["handeye"]
print(f" 手眼:{he.get('file')}")
if he.get("translation_m"):
t = he["translation_m"]
print(f" 平移 [{t[0]:+.4f}, {t[1]:+.4f}, {t[2]:+.4f}] m")
for p in he.get("problems") or []:
print(f" [FAIL] {p}")
problems.append(p)
if not (he.get("problems") or []):
print(" [OK ] 矩阵自洽(正交、det=+1、量级合理)")
hv = rep["hover"]
print(
f" 悬停几何:法兰 {hv['hole_hover_z_m']:.4f} − 工具长 "
f"{hv['tool_tip_below_flange_m'] * 1000:.1f}mm → 尖端 "
f"{hv['hole_hover_z_m'] - hv['tool_tip_below_flange_m']:.4f},"
f"工件面 {hv['workpiece_surface_z_m']:.4f}"
+ ("(未标定)" if hv.get("uncalibrated_surface") else "")
)
for p in hv.get("problems") or []:
print(f" [FAIL] {p}")
problems.append(p)
if not (hv.get("problems") or []):
print(
f" [OK ] 悬停时工具尖端在工件面上方(几何修正值 "
f"{hv['suggested_z_m']:.4f})"
)
return 1 if problems else 0
if __name__ == "__main__":
raise SystemExit(main())