Files
ros_contorl/scripts/lib/rdk_status.py
T
arm_dev 352270d1cc 真机开工体检的编排:把「不报错但不对」的故障源排成现场能照着做的顺序
前一个提交给了四个单项判据(USB / V4L / RDK / 手眼)。这一批把它们的**判据入口**
补齐并编排成一条命令,产出「结论 + 要人工做的事」:

scripts/lib/real_preflight.py
  run_all() 按**现场该处理的顺序**排出结论,不是按代码顺序:
    现场配置 → 遗留进程 → 网络 → 机械臂 RDK → 腕部相机 → USB/触觉/几何
  顺序是有理由的:遗留的 ros2_control 占着 RDK,会让后面每一项的现象都指向别处,
  所以先查它;机械臂能不能动是唯一的人工硬阻断,排在相机之前。

  新增 check_cameras —— **本臂配置的那只腕部相机在不在 USB 上**。
  这是本次实测暴露的判据缺口:2026-09-28 现场 D405 整个从 lsusb 消失
  (只剩顶置 D435),而旧体检**全绿** —— check_usb 只看协商速率,遍历的是
  「发现到的」设备,对「这只根本不在了」无话可说。缺 D405 之后踩的是完全不同
  的一类错误(点云为空 / 找不到工件),排查方向会被带偏很远。
  按 site.yaml 的配置意图核对:robots[<sn>].camera.name → cameras.<name>.model,
  比**型号**而不是序列号 —— 实测这台 D435 的 sysfs serial 是空串,按序列号判会
  永远判「不在」;序列号只在型号对上时做一次「是不是本臂那只」的提醒(WARN)。

  修掉三处会误导现场的输出:
  · _run() 改成三返回值(分离 stdout/stderr,RDK 的 C++ 日志会混进 stdout)
    之后,_ps_output/网络两处还在按两返回值解包 —— 体检**第一步就崩**,
    什么结论都给不出来。现在有 test_run_all_completes_without_crashing 钉住。
  · 人工动作里「去查网线」出现过两条(check_network 与 rdk_status 各写一句
    意思相同、字面不同的话),去重是按整串文字做的,于是人读到两条、还得判断
    「这是一个问题还是两个」。措辞抽到 manual_actions.py,两边字面一致。
  · 触觉 wedged 的处置与 v4l_health 同步(免密的 reset 是首选,重插是兜底)。

scripts/lib/manual_actions.py(新)
  人工动作措辞的**唯一**来源。判据分在两个文件、还跑在两个不同的 Python 上
  (3.11 / 3.10,见 rdk_status 的模块说明),互相 import 不了,所以抽成只依赖
  标准库的公共模块。

scripts/run_real_preflight.sh(新)
  一条命令的入口:只读、不动臂,不需要机械臂能连上。

实测(2026-09-28,arm 89):体检跑通并给出 NO-GO + 7 条互不重复的人工动作,
新判据抓到「D405 不在 USB 上」(旧版全绿)。同时确认了本机可用的部分:
夹爪 PGEA 初始化完成 / 全开 20.00mm、右指触觉 SDK 300 帧 @29.9Hz、
手眼矩阵自洽、enp4s0 千兆全双工。

测试:test_real_preflight 28 项通过(含跨模块的措辞去重回归、
编排端到端冒烟、腕部相机四种情形)。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 10:42:49 +08:00

187 lines
6.8 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
"""读一次机械臂的"能不能动"状态,并给出**人话结论**。
必须在 ``/home/cc/rdk_py310/bin/python`` 下跑(flexivrdk 与 ROS 的 3.11 ABI 不兼容)。
为什么要有这个脚本
------------------
``run_real.sh`` 起来之后,如果机器人没使能,``ros2_control`` 会**卡在**
``on_activate`` 里等 ``operational``(``wait_for_operational`` 是死循环重试)。
卡住的表现是:
· ``ros2 control list_controllers`` → ``Could not contact service``(不是"没起来")
· ``/joint_states`` 一条都不发
· 日志里每 5 秒一句 ``Waiting for operational (status: ...)``
实测(2026-09-28):状态是 ``IN_MANUAL_MODE`` —— 注意 ``robot.mode()`` 返回的是
``Mode.IDLE``,**看不出**这点,必须读 ``operational_status()``。示教器那个 Auto/Manual
开关没有 RDK 接口可以远程切(Flexiv 的安全设计),所以只能人到现场拨。
这个脚本的价值就是把"卡住了"翻译成"去拨那个开关",而不是让人去查 ROS。
输出
----
``--out FILE`` 写 ``KEY=VALUE``(RDK 自己的日志会混在 stdout 里,所以结果单独落文件,
不去解析第三方日志)。失败分类用 :func:`classify_state`(纯函数,可单测)。
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Dict, List, Optional, Sequence, Tuple
# rdk_status.py 必须在 **3.10** 下跑(flexivrdk 与 ROS 的 3.11 ABI 不兼容),
# 所以这里只 import 标准库 + 同目录下同样只用标准库的 manual_actions。
sys.path.insert(0, str(Path(__file__).resolve().parent))
from manual_actions import ( # noqa: E402
NET_UNREACHABLE_ACTION,
RDK_REMOTE_MODE_ACTION,
RDK_STACK_HOLDING_ACTION,
)
# 结论级别:ok / warn / fail(与 scripts/lib/usb_health.py 同一套词)
LEVEL_OK = "ok"
LEVEL_WARN = "warn"
LEVEL_FAIL = "fail"
# 现场那句"必须人去拨"的说明。preflight 与文档引用同一句话。
MANUAL_HINT = (
"示教器切到 Auto(Remote)—— RDK **没有**远程切 Manual→Auto 的接口,"
"这是 Flexiv 的安全设计,必须人到现场拨。切完**不用**重启栈:"
"run_real.sh 里的 wait_for_operational 会自动接着 Enable。"
)
def classify_state(
*,
connected: bool,
operational: bool = False,
op_status: str = "",
mode: str = "",
fault: bool = False,
estop_released: bool = True,
error: str = "",
) -> Tuple[str, str, List[str]]:
"""把机器人状态翻成 ``(级别, 一句话结论, 建议动作)``。
判定顺序是**故意的**:故障/急停要排在模式前面 —— 急停没复位时模式可能还是
Auto,只报"模式不对"会让人白跑一趟。
"""
if not connected:
return (
LEVEL_FAIL,
f"连不上 RDK:{error or '未知原因'}",
[
# 措辞来自 manual_actions:体检报告里 check_network 也会给出
# 同一条,两处字面一致才能去重成一条(否则人读两遍)。
NET_UNREACHABLE_ACTION,
RDK_REMOTE_MODE_ACTION,
RDK_STACK_HOLDING_ACTION,
],
)
if fault:
return (
LEVEL_FAIL,
"机器人有故障(fault)",
[
"在示教器上清故障;没有示教器就重启控制柜。",
"栈里的 wait_for_operational 也会自动 ClearFault 重试。",
],
)
if not estop_released:
return (
LEVEL_FAIL,
"急停没复位(estop_released=False)",
["松开示教器/控制柜上的急停按钮,再按面板上的复位。"],
)
upper = (op_status or "").upper()
if "MANUAL" in upper:
return (
LEVEL_FAIL,
f"示教器在 Manual 模式(operational_status={op_status})—— "
"机器人不会使能,整条流程会一直等着",
[MANUAL_HINT],
)
if operational:
return (LEVEL_OK, f"已使能、可接收指令(mode={mode})", [])
return (
LEVEL_FAIL,
f"未使能(operational_status={op_status or '?'}, mode={mode or '?'})",
[
MANUAL_HINT,
"确认没有未清的故障、急停已复位。",
],
)
def _probe(sn: str, iface: str, enable: bool) -> Dict[str, object]:
"""连一次机器人并读状态。返回给 classify_state 的关键字参数。"""
import flexivrdk # 延迟导入:让本模块在没有 flexivrdk 的环境里也能被单测导入
out: Dict[str, object] = {
"connected": False,
"operational": False,
"op_status": "",
"mode": "",
"fault": False,
"estop_released": True,
"error": "",
}
try:
if iface:
robot = flexivrdk.Robot(sn, [iface], True, False)
else:
robot = flexivrdk.Robot(sn)
except Exception as exc: # noqa: BLE001
out["error"] = str(exc)
return out
out["connected"] = True
if enable:
try:
robot.Enable()
except Exception as exc: # noqa: BLE001
out["error"] = f"Enable 失败: {exc}"
def _safe(fn, default):
try:
return fn()
except Exception: # noqa: BLE001
return default
out["mode"] = str(_safe(robot.mode, ""))
out["operational"] = bool(_safe(robot.operational, False))
out["op_status"] = str(_safe(robot.operational_status, ""))
out["fault"] = bool(_safe(robot.fault, False))
out["estop_released"] = bool(_safe(robot.estop_released, True))
return out
def main(argv: Optional[Sequence[str]] = None) -> int:
ap = argparse.ArgumentParser(description="读一次机械臂状态并给结论")
ap.add_argument("--sn", required=True)
ap.add_argument("--iface", default="")
ap.add_argument("--enable", action="store_true", help="顺手尝试 Enable(会真的使能)")
ap.add_argument("--out", default="", help="把 KEY=VALUE 写到这个文件")
ap.add_argument("--json", action="store_true", help="把结果打到 stdout(JSON)")
args = ap.parse_args(argv)
state = _probe(args.sn, args.iface, args.enable)
level, summary, actions = classify_state(**state) # type: ignore[arg-type]
payload = {**state, "level": level, "summary": summary, "actions": actions}
if args.out:
lines = [f"{k}={v}" for k, v in payload.items() if k != "actions"]
lines.append("actions=" + " | ".join(actions))
Path(args.out).write_text("\n".join(lines) + "\n", encoding="utf-8")
if args.json:
print(json.dumps(payload, ensure_ascii=False, indent=2))
return 0 if level == LEVEL_OK else 1
if __name__ == "__main__":
sys.exit(main())