跳转至

第 1 章 操作对象与栈定位

操作(manipulation)指机械臂改变自身与物体之间的相对位姿并让这种改变生效的那段工作:抓取、插入、装配、放置。约束它的第一件事不是算法,而是机器本体的物理预算——负载、可达范围与关节速度;第二件事是软件栈的分层——谁出轨迹、谁闭环、谁负责安全。两条边界可一次划清:给出选型三轴与四类常见臂型的适用分界,区分哪些数能从模型文件里直接读出、哪些只能取自厂商手册,并按实际启动的进程与接口读数给出任务级规划(task-level planning)在 ROS 2 栈中的位置。

MoveIt 2 栈里的四个核心件与两条外部输入:

flowchart LR
    app["应用 / 任务脚本"] --> mg["move_group<br/>规划与执行接口"]
    mg --> ps["PlanningScene<br/>机器人 + 世界 + ACM"]
    mg --> pl["规划器插件<br/>OMPL / Pilz"]
    mg --> ctl["控制器接口<br/>ros2_control"]
    sens["传感器 / 点云"] -.->|"更新占据地图"| ps

1. 臂型与选型三轴

选型三轴是额定负载(rated payload)、工作空间半径(workspace reach)与重复定位精度(repeatability)。三者互相牵制:同一台机器,负载越大可用的加速度越小;臂展越大,末端刚度越低、精度越难维持。

类别 结构特征 适用 不适用
协作臂(cobot) 关节轻量化、常配力矩传感,接触力与功率按 ISO/TS 15064 限值设计 与人共台的桌面操作、换产频繁的柔性工位 高节拍、大负载
六轴工业臂 高刚度减速器、封闭工作单元 快速搬运、装配、焊接与涂胶 无围栏的人机共台
SCARA 水平面内两个旋转关节加一个直接驱动的竖直关节,四自由度 平面拾放与轴孔插入 需要任意空间姿态的曲面作业
delta 三条主动支链加一条从动中心支链,电机全部集中于基座,动平台轻 轻载高速分拣,抓取节拍以每秒数次计 需要大接触力或大下压的作业

三轴的读数条件各不相同。额定负载在手册上通常附带力臂与姿态条件,末端加装夹爪与法兰后,可用负载要按夹爪重量与质心外移量一起折算;工作空间半径描述的是最大伸展,规划里真正可用的是扣除奇异位形与本体遮挡后的有效子集(可操作度判据见 robotics-note 第 4 章);重复定位精度按 ISO 9283 的定义是在规定姿态、规定负载、规定速度下多次重复到达同一目标点的散布,它不等于绝对精度。

一个常见误解需要在此纠正:把重复定位精度当作抓取成败的唯一硬件变量。视觉引导的操作链路里,工件的绝对位置由感知在线重估,几十分之一毫米的机械散布远小于物体位姿估计的误差,机械精度反而不是瓶颈;此时更该问的是手眼外参的标定质量与残余不确定度往哪里传播(camera-note 第 1 章)。无引导的纯示教重复作业则相反,机械散布直接落在放置精度上。

2. 模型里读得到的运动预算

规划器消费的机器人边界只有一组数:关节位置上下限、速度上限、加速度上限(关节类型按 URDF 记法为 revolute 旋转关节与 prismatic 移动关节)。它们分散在两个文件里,后者覆写前者——URDF 的 <limit> 元素带位置、速度、力矩三项,但 URDF 语法里没有加速度与加加速度;MoveIt 侧用 joint_limits.yaml 补上并覆写速度。以运行环境 ROS 2 Humble + MoveIt 2 2.5.10 的样本资产 moveit_resources_panda_moveit_config 为例:

关节 类型 URDF 位置区间(rad;移动关节为 m) URDF 速度上限 覆写后速度 加速度上限
panda_joint1 revolute −2.9671 ~ 2.9671 2.3925 2.1750 3.75
panda_joint2 revolute −1.8326 ~ 1.8326 2.3925 2.1750 1.875
panda_joint3 revolute −2.9671 ~ 2.9671 2.3925 2.1750 2.5
panda_joint4 revolute −3.1416 ~ 0.0873 2.3925 2.1750 3.125
panda_joint5 revolute −2.9671 ~ 2.9671 2.8710 2.6100 3.75
panda_joint6 revolute −0.0873 ~ 3.8223 2.8710 2.6100 5.0
panda_joint7 revolute −2.9671 ~ 2.9671 2.8710 2.6100 5.0
panda_finger_joint1/2 prismatic 0.0 ~ 0.04 0.2 0.1 未设限

七个臂关节的覆写值统一取 URDF 值的 90.9%(2.1750 / 2.3925 = 2.6100 / 2.8710 ≈ 0.909),是留出的降额余量;两个移动关节取一半。单位按 REP 103 约定:旋转关节用弧度、移动关节用米,0.04 是单指 4 cm 的行程。

两处差异可由一段脚本对读:

import xml.etree.ElementTree as ET
import yaml

## URDF 侧:xacro 处理后的 <limit> 元素,带位置/速度/力矩,无加速度
## 模型文件的生成:xacro <配置包>/config/panda.urdf.xacro > panda.urdf
root = ET.parse("panda.urdf").getroot()
urdf = {j.get("name"): j.find("limit").attrib
        for j in root.findall("joint") if j.find("limit") is not None}

## MoveIt 侧:joint_limits.yaml 覆写速度并补加速度上限
cfg = yaml.safe_load(open("joint_limits.yaml"))["joint_limits"]

for name, lim in urdf.items():
    over = cfg.get(name, {})
    v_urdf, v_cfg = float(lim["velocity"]), over.get("max_velocity")
    print(f"{name:22s} URDF {v_urdf:.4f} → 覆写 {v_cfg} "
          f"比值 {v_cfg / v_urdf:.4f}" if v_cfg else f"{name:22s} 未覆写")

输出里 比值 0.9091 出现七次、0.5000 出现两次,就是上表最后两列的来源。这组数在配置里的最终落点是 robot_description_planning 参数命名空间下的 joint_limits——时间参数化(time parameterization,第 4 章)拿速度与加速度重定时,越界检查拿它判起点合法性。URDF 里的力矩上限(87 / 12 N·m 与 20 N)在这一层不参与:它属于驱动器与电流环的量(hardware-note 第 2、3 章)。

可达范围在模型里不是一个数,而是随组态变化的量。panda 样本 demo 用 initial_positions.yaml 把假硬件的起点写成 SRDF 的 ready 组态,此时基座到末端安装面 panda_link8 的距离可直接从 TF 树查:

## 运行环境 ROS 2 Humble,demo.launch.py 已在后台
ros2 run tf2_ros tf2_echo panda_link0 panda_link8
- Translation: [0.307, -0.000, 0.590]

合位移 \(\sqrt{0.307^2 + 0.590^2} = 0.665\ \text{m}\)。把七个关节全部置零,同一模型给出的是另一组数:平移 \([0.088, -0.000, 0.926]\)、合位移 0.930 m。两者都不等于厂商标称的臂展——后者按最大伸展姿态与指定测量基准面定义,模型侧只能算“给定组态下的距离”。两个数可由一段正运动学(forward kinematics)累乘一次算出,与上面 TF 的结果一致:

import math
import numpy as np
from urdf_parser_py.urdf import URDF       # 运行环境 ROS 2 Humble

robot = URDF.from_xml_file("panda.urdf")   # xacro 处理后的模型文件
by_name = {j.name: j for j in robot.joints}

def rpy_matrix(r, p, y):                   # URDF 约定:固定轴外旋 R = Rz·Ry·Rx
    Rx = np.array([[1, 0, 0], [0, math.cos(r), -math.sin(r)], [0, math.sin(r), math.cos(r)]])
    Ry = np.array([[math.cos(p), 0, math.sin(p)], [0, 1, 0], [-math.sin(p), 0, math.cos(p)]])
    Rz = np.array([[math.cos(y), -math.sin(y), 0], [math.sin(y), math.cos(y), 0], [0, 0, 1]])
    return Rz @ Ry @ Rx

def joint_motion(j, q):                    # revolute 绕 axis 转 q 弧度,prismatic 沿 axis 移 q 米
    ax = np.array(j.axis or [0, 0, 1], float); ax /= np.linalg.norm(ax)
    T = np.eye(4)
    if j.type == "revolute":
        c, s, t, (x, y, z) = math.cos(q), math.sin(q), 1 - math.cos(q), ax
        T[:3, :3] = [[t*x*x + c,   t*x*y - s*z, t*x*z + s*y],
                     [t*x*y + s*z, t*y*y + c,   t*y*z - s*x],
                     [t*x*z - s*y, t*y*z + s*x, t*z*z + c]]
    elif j.type == "prismatic":
        T[:3, 3] = ax * q
    return T

def tip_pose(positions):                   # 沿 link0 → link8 单链累乘:安装位姿 ∘ 关节运动
    T, link = np.eye(4), "panda_link0"
    while link != "panda_link8":
        jname, link = robot.child_map[link][0]      # panda 每段只有唯一子关节
        j = by_name[jname]
        M = np.eye(4); M[:3, :3] = rpy_matrix(*j.origin.rpy); M[:3, 3] = j.origin.xyz
        T = T @ M
        if j.type in ("revolute", "prismatic"):
            T = T @ joint_motion(j, positions.get(j.name, 0.0))
    return T

ready = dict(zip([f"panda_joint{i}" for i in range(1, 8)],
                 [0.0, -0.785, 0.0, -2.356, 0.0, 1.571, 0.785]))
for label, qs in (("全零位", {}), ("ready", ready)):
    p = tip_pose(qs)[:3, 3]
    print(f"{label} 平移=[{p[0]:.3f}, {p[1]:.3f}, {p[2]:.3f}] 距离={np.linalg.norm(p):.3f} m")
全零位 平移=[0.088, -0.000, 0.926] 距离=0.930 m
ready 平移=[0.307, -0.000, 0.590] 距离=0.665 m

ready 行与 TF 读数逐位一致,说明两边读的是同一份几何。要关心的不是这两个数本身,而是目标点能否落在可达集内——扣掉奇异位形与本体自碰撞后的可用子集,明显小于以这两个读数画出的球壳(第 3、4 章)。

3. 规划框架与实时控制的分层

MoveIt 2 的准确定位是任务级规划框架:它的输入是模型描述与环境的碰撞表示,输出是一条带时间戳的关节轨迹;它不在闭环里。一次完整链路的分层与各层频率量级如下。

层 载体 周期量级
模型与环境 URDF/SRDF、robot_state_publisher、规划场景(planning scene) 事件驱动
规划 move_group 节点或进程内规划组件 + 规划管线(planning pipeline) 10⁻³ ~ 10⁻¹ s 一次请求
时间参数化 规划管线内的重定时适配器 与规划同批
轨迹执行 controller_manager + joint_trajectory_controller 10 ms(100 Hz)
关节伺服 驱动器内部位置/速度/电流环 千赫兹级(约 10⁻⁴ s)

用 panda 样本 demo 起来后的实际拓扑可把这张表落到进程与接口上:

ros2 action list && ros2 control list_controllers
/execute_trajectory
/move_action
/panda_arm_controller/follow_joint_trajectory
/panda_hand_controller/gripper_cmd

panda_hand_controller   position_controllers/GripperActionController           active
joint_state_broadcaster joint_state_broadcaster/JointStateBroadcaster          active
panda_arm_controller    joint_trajectory_controller/JointTrajectoryController  active

四个动作分属两层:/move_action(moveit_msgs/action/MoveGroup)与 /execute_trajectory(moveit_msgs/action/ExecuteTrajectory)是 MoveIt 对外的规划与执行入口;/panda_arm_controller/follow_joint_trajectory 才是把轨迹交给 ros2_control 的那条通路,gripper_cmd 是夹爪的对应通路。

controller_manager 的 update_rate 在配置里写死为 100 Hz,panda_arm_controller 的 command_interfaces 只列 position、state_interfaces 列 position 与 velocity——轨迹里的速度前馈在这一层不进入指令,控制器只做位置插值跟随。执行链的细节归第 7 章,此处只划边界。

三个规划器在同一请求上的读数把量级差摊开(运行环境 ROS 2 Humble + MoveIt 2 2.5.10 + OMPL 1.7.0,进程内接口,起点取当前状态——臂此时停在 ready,目标为 SRDF 组态 extended):

规划器 规划耗时 航点数 轨迹时长
OMPL / RRTConnect 0.004 s 19 1.779 s
OMPL / PRM 0.013 s 19 1.779 s
Pilz / PTP 0.001 s 24 2.242 s

出解在毫秒量级,重定时后的轨迹在秒量级,执行环在 10 ms 量级——三个数量级互不重叠,这正是“规划与实时执行分层”的实证。两个可注意的现象:OMPL 与 Pilz 给出同样的起点终点却有不同的航点数与时长,因为二者的轨迹整形规则不同;把目标换成 SRDF 里另一组态 transport 时,三个规划器在同一份配置下都不出解。前者在第 4 章展开;后者的成因已定位——transport 本身就是一个非法位形(panda_link7 与 panda_link5 相交 4.4 mm),第 2 章把它作为“具名组态不保证合法”的实例使用。

MoveIt 2 并不直接控制机械臂:MoveIt 出轨迹,100 Hz 的位置跟随由 joint_trajectory_controller 完成,再往下还有驱动器内部更高频率的环。MoveIt 不参与周期内的闭环:执行动作返回时轨迹已经跑完,期间的每个伺服周期都由控制器决定。把两者混同的后果在第 7 章会以“改规划器参数去消除跟踪抖动”的形式出现。

4. 发行版配对与安装形态

主线运行环境是 ROS 2 Humble(Ubuntu 22.04)。MoveIt 2 的发布形态按发行版分列,Humble 侧上限随 apt 滚动,当前读数 2.5.10;Rolling 侧已到 2.15.x。栈的安装由若干独立包组成,各自承担一个接口面:

包 版本 角色
moveit 2.5.10 元包,拉齐规划框架本体
moveit-py 2.5.10 MoveIt 2 的进程内 Python 绑定(PyBinding)
pymoveit2 5.0.0 社区维护的 Python 封装,通过动作与服务与 move_group 交互
moveit-task-constructor-* 0.1.3 任务构造器(MoveIt Task Constructor, MTC)的编排框架
moveit-servo 2.5.10 实时伺服执行,直接消费速度指令
pilz-industrial-motion-planner 2.5.10 工业规划器,提供 PTP / LIN / CIRC
moveit_setup_assistant 2.5.10 配置生成向导
moveit-resources-panda-* 2.0.7 示例机械臂模型与配置
ros2-control / ros2-controllers 2.54.3 / 2.54.1 执行侧框架与控制器
ros-gz 0.244.26 物理仿真接入

两条安装前提需要记录。其一,moveit-py 与 MTC 的 Python 绑定共用命名空间包(namespace package)moveit,且都提供 moveit/__init__.py,同装时包管理器会报文件占用冲突,需允许覆盖后才能共存;共存后 moveit.planning 与 moveit.task_constructor 均可正常导入。其二,进程内绑定与 pymoveit2 是两套接口面:前者在本进程内装配规划组件,后者把请求发给独立的 move_group 节点,两者的失败形态不同(第 4、5 章分别使用)。

逆运动学(inverse kinematics, IK)求解器的发行形态不对称:KDL 随 moveit_kinematics 一并发行,ikfast 需为目标臂生成代码、无预编译包,trac-ik 在 Humble 无发行版包、须源码编译。因此涉及求解器差异的结论只在可核实范围内给(第 2 章)。

5. 全链路与板块分界

一条桌面抓放链路可以拆成四个环节,每个环节的输入输出都有确定的形状:

输入   模型(URDF + SRDF)、环境(规划场景)、感知(物体位姿 / 点云)
          ↓
规划   move_group 出解 → trajectory_msgs/JointTrajectory
          ↓
执行   controller_manager + joint_trajectory_controller → 关节运动
          ↓
反馈   joint_states + TF → 当前状态与规划场景更新(回到规划)
环节 本板块位置 归属其他板块
本体几何与物理 第 2 章的工程形态 robotics-note 第 4、5 章(雅可比、静力对偶、IK 推导)
关节回路与控制律 第 7 章的消费关系 control-note 第 1、5、10 章
物体位姿与点云 第 6 章的接口形状 camera-note 第 1、2、5 章
器件与选型物理 第 1 章的应用侧 hardware-note 第 1–4 章
话题 / TF / 控制器机制 全篇直接使用 ROS2-note 第 5、6 章
学习式策略训练 第 9 章判据 isaac-note 第 5、7 章
关节伺服总线 第 2 章一句名目 fieldbus-note 第 5 章

分界的判据是“谁持有状态”:MoveIt 持有规划用的世界模型与机器人模型副本,不持有力学状态;ros2_control 持有执行周期内的目标与实际值;驱动器持有电流环。三处状态需要同步但不互相代替。

6. 仿真路的三个层次

操作的验证离不开仿真,三层能验的东西不同,选错层会让结论失效。panda 样本的 demo.launch.py 把这一层的选择显式做成参数:

## demo.launch.py 中(节选):硬件接口类型可由启动参数切换
ros2_control_hardware_type = DeclareLaunchArgument(
    "ros2_control_hardware_type",
    default_value="mock_components",
    description="ROS2 control hardware interface type to use for the launch file "
                "-- possible values: [mock_components, isaac]",
)
层次 载体 能验 验不了
无物理 mock_components(demo 默认) 规划正确性、配置通路、接口与消息形状 跟踪误差、接触、时延
物理仿真 ros_gz 0.244.26 碰撞响应、抓取滑移、执行滞后 真实驱动器的抖动与量化
GPU 并行 Isaac Lab 大规模采样、强化学习训练 精细接触与真实力控信号

第一层的关节状态由“指令即到达”的假硬件回填,误差恒为零,所以任何关于“执行偏航”“到位超差”的结论在这一层都不可能暴露——这是第 7 章必须跨到物理层的直接原因。第二层的接入机制与桥接细节归 ROS2-note 第 9 章,第三层归 isaac-note 第 1 章,本板块只在需要接触结论时使用第二层。

7. 要点

  • 选型三轴(额定负载、工作空间半径、重复定位精度)互相牵制,且都是手册量;规划器真正消费的是位置、速度与加速度上限,两处来源需分开读。
  • 运动预算分布在 URDF <limit> 与 joint_limits.yaml 两处,后者覆写速度并补上 URDF 语法里没有的加速度上限;样本资产里七个臂关节统一降额到 URDF 值的约 90.9%。
  • URDF 的力矩上限不进规划,它属于驱动器;可达范围在模型侧只能以“指定组态下的基座→末端安装面距离”表达(同一模型给出 0.665 m 与 0.930 m 两个读数),不等于手册臂展。
  • MoveIt 2 是任务级规划框架,出的是带时间的关节轨迹;100 Hz 的位置跟随由 ros2_control 侧的 joint_trajectory_controller 完成,MoveIt 本身不参与周期内闭环。
  • 一次链路的三个时间量级互不重叠:出解毫秒、轨迹秒级、执行环十毫秒级;分层误用会表现为“调规划器参数去消除跟踪抖动”。
  • 安装形态由多个独立包组成,moveit-py 与 MTC 共用 moveit 命名空间包,同装需允许文件覆盖;IK 侧只有 KDL 开箱可用,trac-ik 在 Humble 须源码编译。
  • 仿真三层的判据是“要验什么”:接口与规划用无物理层,接触与滞后用物理层,大规模采样与训练用 GPU 层;无物理层的误差恒为零,出不了执行类结论。

参考文献与延伸阅读

  • Siciliano, B., & Khatib, O. (Eds.). Springer Handbook of Robotics (2nd ed.). Springer, 2016. DOI
  • MoveIt. 项目门户与文档入口. 文档
  • Corke, P. Robotics, Vision and Control (2nd ed.). Springer, 2017. DOI