1. 为什么我最终选择了 pymoveit2 而不是原生 MoveIt2 接口
机械臂控制这件事,说简单也简单,说复杂也复杂。简单在于,如果你只是想让机械臂从 A 点走到 B 点,用 MoveIt2 的 C++ 接口写几十行代码也能跑起来。复杂在于,当你真正要把机械臂接入一个完整的应用系统,需要跟视觉、任务调度、状态机、Web 后端这些东西打通的时候,C++ 的编译周期和开发效率就会变成一种折磨。
我最早做机械臂项目的时候,用的是 MoveIt2 原生 C++ 接口。MoveGroupInterface 那一套 API 确实成熟,文档也全,但每次改一个参数就要重新 colcon build,等编译的时间比写代码的时间还长。后来接触到 pymoveit2 这个 Python 封装库,一开始我是持怀疑态度的——Python 调 ROS 2 的实时性够不够?封装层会不会丢功能?实际用下来发现,对于绝大多数非硬实时的机械臂控制场景,pymoveit2 完全够用,而且开发效率提升非常明显。
pymoveit2 本质上是对 MoveIt2 的 C++ 接口做了一层 pybind11 绑定,把 MoveGroupInterface、PlanningSceneInterface 这些核心类暴露给 Python。它不是简单的 ROS 2 Python 客户端封装,而是直接调用 MoveIt2 的 C++ 核心,所以规划质量和 C++ 接口是一致的。这一点很关键,很多人误以为 Python 封装就意味着性能打折,实际上规划本身还是在 C++ 层跑的,Python 只是负责调用和参数传递。
这篇文章我打算把 pymoveit2 的核心用法、参数配置、常见坑点、以及我在实际项目中积累的一些控制技巧完整地梳理一遍。不管你是刚接触 ROS 2 和 MoveIt2 的新手,还是已经用 C++ 接口做过项目想转 Python 的老手,应该都能从中找到有用的东西。文章会涉及环境搭建、核心 API 解析、笛卡尔空间控制、避障规划、以及多段轨迹拼接这些实战内容,代码都会给完整的可运行版本。
1.1 pymoveit2 到底封装了什么
要理解 pymoveit2 的能力边界,得先搞清楚它封装了 MoveIt2 的哪些部分。从源码结构来看,pymoveit2 主要提供了两个核心类:MoveIt2和MoveIt2Servo。
MoveIt2类封装的是 MoveGroupInterface 的功能,包括关节空间规划、笛卡尔空间规划、位姿目标设置、规划场景管理、碰撞检测等。你可以在 Python 里直接设置目标关节角度、目标末端位姿、规划组名称、末端执行器连杆名称这些参数,然后调用move_to_configuration()或move_to_pose()来执行规划。
MoveIt2Servo类封装的是 Servo 功能,用于实时伺服控制。这个在需要遥操作或者视觉伺服的时候特别有用,它可以接收笛卡尔速度指令,以较高的频率发送给机械臂控制器。
还有一个比较实用的功能是compute_fk()和compute_ik(),分别用于正运动学和逆运动学计算。这两个在标定和坐标变换验证的时候经常用到。
注意:pymoveit2 的版本需要和你的 ROS 2 发行版匹配。目前 Humble 和 Iron 都有对应的分支,Jazzy 的支持也在逐步完善。装错版本会出现 API 不匹配的问题。
1.2 和原生 C++ 接口的对比
我用一个实际的例子来说明差异。假设要实现一个简单的关节空间规划,从当前位置移动到一组目标关节角度。
C++ 版本的代码大概是这样:
auto move_group = std::make_shared<moveit::planning_interface::MoveGroupInterface>(node, "arm_group"); move_group->setJointValueTarget(target_joints); moveit::planning_interface::MoveGroupInterface::Plan plan; auto success = (move_group->plan(plan) == moveit::core::MoveItErrorCode::SUCCESS); if (success) { move_group->execute(plan); }pymoveit2 版本:
from pymoveit2 import MoveIt2 moveit2 = MoveIt2( node=node, joint_names=['joint1', 'joint2', 'joint3', 'joint4', 'joint5', 'joint6'], base_link_name='base_link', end_effector_name='tool0', group_name='arm_group', ) moveit2.move_to_configuration(target_joints)代码量少了大概一半,而且不需要编译。对于快速原型开发来说,这个效率差距在项目初期非常明显。当然,C++ 版本在需要精细控制规划器参数、自定义规划适配器的时候更灵活,但 pymoveit2 也暴露了大部分常用参数,够用。
2. 环境搭建:从零到能跑通第一个例子
环境搭建这部分我踩过的坑最多,所以单独拿出来详细说。很多人卡在环境配置上就放弃了,其实只要版本对齐,整个过程并不复杂。
2.1 ROS 2 和 MoveIt2 的安装
假设你用的是 Ubuntu 22.04,对应 ROS 2 Humble。这是目前最稳定的组合,社区支持也最好。
先装 ROS 2 Humble 的基础包:
sudo apt update sudo apt install ros-humble-desktop然后装 MoveIt2:
sudo apt install ros-humble-moveit这里有个细节要注意,ros-humble-moveit是一个元包,它会拉取 MoveIt2 的核心组件,包括 moveit_core、moveit_ros_planning_interface、moveit_ros_move_group 等。如果你只需要规划功能,不需要 RViz 插件,可以只装ros-humble-moveit-core和ros-humble-moveit-ros-planning-interface,能省不少空间。
装完之后验证一下:
ros2 pkg list | grep moveit应该能看到一堆 moveit 相关的包。如果什么都没输出,说明环境变量没 source,执行:
source /opt/ros/humble/setup.bash2.2 pymoveit2 的安装方式选择
pymoveit2 的安装有两种方式:pip 安装和源码安装。我强烈建议用源码安装,原因有两个:一是 pip 上的版本更新不及时,可能和你的 MoveIt2 版本不匹配;二是源码安装方便你直接看源码,遇到问题能快速定位。
源码安装步骤:
cd ~/ros2_ws/src git clone https://github.com/AndrejOrsula/pymoveit2.git cd ~/ros2_ws rosdep install -y --from-paths src --ignore-src colcon build --symlink-install source install/setup.bash--symlink-install这个参数很重要,它创建的是符号链接而不是拷贝文件,这样你修改 Python 源码后不需要重新 build 就能生效,调试的时候非常方便。
提示:如果你之前装过 pip 版本的 pymoveit2,建议先卸载,避免路径冲突。
pip uninstall pymoveit2执行一下。
2.3 验证安装是否成功
写一个最小的测试脚本:
import rclpy from rclpy.node import Node from pymoveit2 import MoveIt2 def main(): rclpy.init() node = Node('test_pymoveit2') moveit2 = MoveIt2( node=node, joint_names=['joint1', 'joint2', 'joint3', 'joint4', 'joint5', 'joint6'], base_link_name='base_link', end_effector_name='tool0', group_name='arm_group', ) node.get_logger().info('pymoveit2 initialized successfully') rclpy.spin(node) rclpy.shutdown() if __name__ == '__main__': main()这个脚本不需要实际的机械臂,只要 MoveIt2 的配置包存在就能初始化。如果报错说找不到 group_name,说明你的 MoveIt2 配置里没有这个规划组,需要检查 SRDF 文件。
3. 核心 API 深度解析与参数配置
pymoveit2 的 API 设计比较直观,但有几个参数如果不理解其含义,很容易配错导致规划失败或者运动异常。这一章我把最核心的几个 API 和参数拆开讲。
3.1 MoveIt2 类的构造参数详解
MoveIt2类的构造函数参数决定了后续所有规划行为的默认配置。我逐个说明:
| 参数名 | 类型 | 说明 | 常见取值 |
|---|---|---|---|
| node | Node | ROS 2 节点对象 | 必须传入 |
| joint_names | list[str] | 规划组的所有关节名 | 按 URDF 中的顺序 |
| base_link_name | str | 基座连杆名 | 'base_link' |
| end_effector_name | str | 末端连杆名 | 'tool0' 或 'ee_link' |
| group_name | str | MoveIt2 规划组名 | SRDF 中定义的名字 |
| joint_min_positions | list[float] | 关节下限 | 可选,默认从 URDF 读取 |
| joint_max_positions | list[float] | 关节上限 | 可选,默认从 URDF 读取 |
| max_velocity | float | 最大速度缩放 | 0.0~1.0 |
| max_acceleration | float | 最大加速度缩放 | 0.0~1.0 |
| callback_group | CallbackGroup | 回调组 | 多线程时需要 |
joint_names的顺序必须和 URDF 中定义的关节顺序一致,否则会出现规划出来的轨迹关节对应错乱的问题。这个坑我在第一次用的时候踩过,机械臂动起来完全是乱的,排查了半天才发现是关节顺序写反了。
max_velocity和max_acceleration是缩放因子,不是绝对速度值。比如设成 0.5,意味着以最大速度的 50% 运行。实际项目中我一般设 0.3 到 0.5,太快了不安全,太慢了效率低。
3.2 关节空间规划:move_to_configuration
这是最常用的接口,给定一组目标关节角度,规划并执行运动。
target_joints = [0.0, -1.57, 0.0, -1.57, 0.0, 0.0] moveit2.move_to_configuration(target_joints)这个函数内部做了几件事:设置关节目标、调用规划器、检查规划结果、执行轨迹。如果规划失败,它会返回 False,但不会抛异常。所以实际使用中一定要检查返回值:
success = moveit2.move_to_configuration(target_joints) if not success: node.get_logger().error('Planning failed')还有一个move_to_configuration的变体是plan_to_configuration,只规划不执行,返回轨迹对象。这个在需要先预览轨迹或者做轨迹后处理的时候有用。
3.3 笛卡尔空间规划:move_to_pose
笛卡尔空间规划给定的是末端执行器的目标位姿,包括位置和姿态。
from geometry_msgs.msg import PoseStamped target_pose = PoseStamped() target_pose.header.frame_id = 'base_link' target_pose.pose.position.x = 0.3 target_pose.pose.position.y = 0.0 target_pose.pose.position.z = 0.4 target_pose.pose.orientation.x = 0.0 target_pose.pose.orientation.y = 0.707 target_pose.pose.orientation.z = 0.0 target_pose.pose.orientation.w = 0.707 moveit2.move_to_pose(target_pose)姿态用四元数表示,很多人不习惯。我一般会用 tf_transformations 库来从欧拉角转换:
from tf_transformations import quaternion_from_euler import math qx, qy, qz, qw = quaternion_from_euler(math.radians(180), 0, 0)笛卡尔规划的成功率比关节空间规划低,因为末端位姿约束更强,可能不存在可行的逆运动学解。实际使用中要做好失败重试和备选方案。
3.4 规划场景管理:添加障碍物
MoveIt2 的规划场景可以动态添加障碍物,pymoveit2 也封装了相关接口。
moveit2.add_collision_box( name='obstacle_box', position=[0.3, 0.0, 0.2], quat_xyzw=[0.0, 0.0, 0.0, 1.0], size=[0.1, 0.1, 0.1], )添加障碍物后,后续的规划会自动避障。这个功能在实际项目中非常重要,尤其是机械臂工作空间内有固定设备或者人员活动区域的时候。
移除障碍物:
moveit2.remove_collision_object('obstacle_box')注意:添加障碍物后,如果障碍物位置和机械臂当前状态有碰撞,规划会直接失败。所以添加障碍物之前要确保机械臂不在碰撞状态。
4. 实战:完整的机械臂控制流程
这一章我用一个完整的例子把前面讲的东西串起来。场景是这样的:机械臂需要从初始位置出发,绕过中间的一个障碍物,到达目标位姿,然后执行一段笛卡尔直线运动。
4.1 场景搭建与初始化
先写一个完整的节点类:
import rclpy from rclpy.node import Node from pymoveit2 import MoveIt2 from geometry_msgs.msg import PoseStamped from tf_transformations import quaternion_from_euler import math import time class ArmController(Node): def __init__(self): super().__init__('arm_controller') self.joint_names = [ 'shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint', ] self.moveit2 = MoveIt2( node=self, joint_names=self.joint_names, base_link_name='base_link', end_effector_name='tool0', group_name='ur_manipulator', max_velocity=0.4, max_acceleration=0.3, ) self.get_logger().info('Arm controller initialized')这里用的是 UR 机械臂的关节名,如果你用的是其他品牌,需要对应修改。group_name也要和你的 SRDF 文件里定义的一致。
4.2 分步骤执行规划任务
我把整个任务拆成几个步骤,每步都有明确的成功判断和日志输出:
def run_task(self): # Step 1: 回到初始位置 home_joints = [0.0, -1.57, 1.57, -1.57, -1.57, 0.0] self.get_logger().info('Moving to home position') if not self.moveit2.move_to_configuration(home_joints): self.get_logger().error('Failed to move to home') return False time.sleep(1.0) # Step 2: 添加障碍物 self.moveit2.add_collision_box( name='obstacle', position=[0.3, 0.0, 0.3], quat_xyzw=[0.0, 0.0, 0.0, 1.0], size=[0.15, 0.15, 0.15], ) self.get_logger().info('Obstacle added') # Step 3: 规划到目标位姿 target = PoseStamped() target.header.frame_id = 'base_link' target.pose.position.x = 0.4 target.pose.position.y = 0.2 target.pose.position.z = 0.5 qx, qy, qz, qw = quaternion_from_euler(math.radians(180), 0, 0) target.pose.orientation.x = qx target.pose.orientation.y = qy target.pose.orientation.z = qz target.pose.orientation.w = qw self.get_logger().info('Planning to target pose') if not self.moveit2.move_to_pose(target): self.get_logger().error('Failed to reach target pose') return False # Step 4: 移除障碍物 self.moveit2.remove_collision_object('obstacle') self.get_logger().info('Task completed') return True这个流程里,每一步都有明确的日志和错误处理。实际项目中,我建议把每个步骤封装成独立的方法,方便复用和调试。
4.3 笛卡尔直线运动实现
有时候关节空间规划出来的轨迹不是直线,末端会走一条弧线。如果需要末端走直线,要用笛卡尔规划。
pymoveit2 提供了move_to_pose的笛卡尔路径选项,但更灵活的方式是用MoveIt2Servo或者手动插值:
def move_linear(self, start_pose, end_pose, steps=50): for i in range(steps + 1): t = i / steps interp = PoseStamped() interp.header.frame_id = 'base_link' interp.pose.position.x = start_pose.pose.position.x + t * (end_pose.pose.position.x - start_pose.pose.position.x) interp.pose.position.y = start_pose.pose.position.y + t * (end_pose.pose.position.y - start_pose.pose.position.y) interp.pose.position.z = start_pose.pose.position.z + t * (end_pose.pose.position.z - start_pose.pose.position.z) interp.pose.orientation = end_pose.pose.orientation self.moveit2.move_to_pose(interp)这种方式本质上是把直线拆成很多小段,每段用笛卡尔规划。缺点是效率低,每段都要规划一次。优点是实现简单,不需要额外的控制器。
实操心得:如果对直线精度要求高,建议用 MoveIt2 的 CartesianPath 接口,pymoveit2 虽然没有直接封装,但可以通过
compute_cartesian_path调用。或者直接用 Servo 模式,实时性更好。
5. 常见问题与排查技巧实录
这一章是我在实际项目中遇到的各种问题汇总,每个问题都附带了排查思路和解决方法。
5.1 规划失败:最常见的原因和排查顺序
规划失败是最高频的问题。我总结了一个排查顺序:
| 排查项 | 检查方法 | 常见问题 |
|---|---|---|
| 目标是否可达 | 手动拖动 RViz 看能否到达 | 目标超出工作空间 |
| 是否碰撞 | RViz 中开启碰撞显示 | 目标位姿和障碍物重叠 |
| 关节限位 | 检查目标关节角度 | 超出 URDF 定义的限位 |
| 规划器配置 | 检查 ompl_planning.yaml | 规划器参数过于保守 |
| 起始状态 | 检查 current_state | 起始状态本身就在碰撞中 |
我遇到最多的情况是目标位姿虽然在工作空间内,但姿态约束导致逆运动学无解。这时候可以尝试放宽姿态约束,或者换一个接近的目标位姿。
5.2 轨迹执行抖动或卡顿
轨迹执行不流畅通常有几个原因:
一是规划器的轨迹点太稀疏,控制器插补跟不上。可以在ompl_planning.yaml里调小longest_valid_segment_fraction,让规划器生成更密集的轨迹点。
二是速度缩放设得太高,控制器响应不过来。把max_velocity降到 0.2 试试。
三是 ROS 2 的通信延迟。如果规划节点和控制器不在同一台机器上,网络延迟会导致轨迹执行不连贯。建议用ros2 topic hz检查一下轨迹话题的发布频率。
5.3 pymoveit2 初始化报错排查
初始化阶段的报错通常和配置有关。常见的几种:
Group 'xxx' not found:SRDF 文件里没有定义这个规划组,或者 group_name 拼写错误。
Joint 'xxx' not found:joint_names 里的关节名和 URDF 不一致。
End effector 'xxx' not found:end_effector_name 对应的连杆在 URDF 里不存在。
Failed to connect to move_group:MoveIt2 的 move_group 节点没启动,或者命名空间不对。
提示:排查这类问题最快的方法是
ros2 param list看一下 move_group 节点加载了哪些参数,对比你的配置。
5.4 多线程与回调组配置
pymoveit2 默认使用单线程执行器,如果你在规划的同时还需要处理其他话题(比如传感器数据),可能会阻塞。这时候需要配置多线程执行器和回调组:
from rclpy.executors import MultiThreadedExecutor from rclpy.callback_groups import ReentrantCallbackGroup callback_group = ReentrantCallbackGroup() moveit2 = MoveIt2( node=node, joint_names=joint_names, base_link_name='base_link', end_effector_name='tool0', group_name='arm_group', callback_group=callback_group, ) executor = MultiThreadedExecutor() executor.add_node(node) executor.spin()这个配置在需要同时处理视觉话题和规划任务的时候特别重要。我一开始没注意这个,导致视觉回调一直被规划阻塞,图像延迟严重。
6. 进阶技巧:让机械臂控制更丝滑
前面讲的都是基础用法,这一章分享几个我在项目中总结的进阶技巧。
6.1 轨迹拼接与连续运动
实际任务中,机械臂往往需要连续执行多个动作。如果每个动作都单独规划执行,中间会有停顿。解决方法是把多段轨迹拼接成一条完整轨迹。
pymoveit2 的plan_to_configuration返回轨迹对象,可以手动拼接:
trajectory1 = self.moveit2.plan_to_configuration(joints1) trajectory2 = self.moveit2.plan_to_configuration(joints2) # 拼接轨迹点 combined = trajectory1 combined.joint_trajectory.points.extend(trajectory2.joint_trajectory.points)拼接的时候要注意时间戳的连续性,第二段轨迹的时间戳要加上第一段的持续时间。否则执行的时候会出现时间跳变。
6.2 基于视觉的动态目标更新
如果目标位姿来自视觉检测,需要动态更新。我的做法是订阅视觉话题,在回调里更新目标位姿,然后用 Servo 模式跟踪:
def vision_callback(self, msg): self.target_pose = msg.pose self.servo_command()Servo 模式的好处是响应快,适合跟踪动态目标。但要注意 Servo 的控制频率和视觉帧率要匹配,否则会出现抖动。
6.3 规划器参数调优
MoveIt2 默认用的是 OMPL 规划器,参数在ompl_planning.yaml里配置。几个关键参数:
planning_time:规划时间上限,默认 5 秒。复杂场景可以调到 10 秒。longest_valid_segment_fraction:轨迹点密度,默认 0.05。调小到 0.01 可以让轨迹更平滑。goal_bias:目标偏置概率,默认 0.05。调大到 0.1 可以加快收敛。
这些参数没有万能值,需要根据具体场景调。我一般先用默认值跑,规划失败率高的时候再逐步调整。
6.4 安全机制设计
机械臂控制最怕的是失控。我在项目里加了几层安全机制:
第一层是速度限制,max_velocity不超过 0.5。
第二层是工作空间限制,在 MoveIt2 配置里设置关节限位和工作空间边界。
第三层是看门狗,如果规划执行超过预期时间,自动发送停止指令。
第四层是碰撞检测,规划场景里始终保留机械臂自身和周围环境的碰撞体。
这些机制看起来繁琐,但真出问题的时候能救命。我见过因为没设速度限制导致机械臂撞坏设备的案例,维修成本远高于加安全机制的时间成本。
6.5 调试工具与可视化
RViz 是必备的调试工具,但光看 RViz 不够。我还会用ros2 topic echo看轨迹话题,用ros2 bag record录数据回放分析。
pymoveit2 的日志级别可以调整,调试的时候设成 DEBUG 能看到规划器的详细输出:
rclpy.logging.set_logger_level('arm_controller', rclpy.logging.LoggingSeverity.DEBUG)还有一个技巧是在规划前后打印时间戳,计算规划耗时。如果规划时间超过 1 秒,说明场景太复杂或者规划器参数需要调。
7. 从仿真到实机的迁移注意事项
仿真跑通了不代表实机就能跑。这一章讲迁移过程中容易忽略的问题。
7.1 坐标系标定
仿真里的坐标系是理想的,实机上 base_link 和实际基座的偏差、tool0 和实际末端工具的偏差都会影响精度。迁移前一定要做手眼标定和工具标定。
标定方法这里不展开,但提醒一点:标定完成后要把结果更新到 URDF 或者通过 TF 发布,否则 MoveIt2 用的还是理想值。
7.2 控制器差异
仿真用的控制器和实机控制器可能不一样。仿真里常用joint_trajectory_controller,实机上可能是厂商专用的控制器。接口不一样,轨迹消息格式也可能有差异。
迁移前先确认实机控制器订阅的话题名和消息类型,必要时写一个转换节点。
7.3 实时性考量
仿真里规划执行的时间是理想化的,实机上受控制器性能、通信延迟、机械惯量影响,实际执行时间会更长。速度缩放要留足余量,我一般实机比仿真再降 30%。
7.4 异常处理
实机上会出现仿真里不会有的异常:通信中断、控制器报错、急停触发。代码里要做好异常捕获和恢复逻辑。我的做法是每个运动指令都包在 try-except 里,出错后先停止运动,再尝试重新连接控制器。
实操心得:实机调试的时候,第一次运行一定要把速度降到最低,手放在急停按钮上。我见过太多第一次跑实机就全速运行导致撞机的案例。
8. 性能优化与代码组织建议
最后聊一下代码层面的优化。pymoveit2 项目做大了之后,代码组织很重要。
8.1 封装控制器类
不要把所有的规划逻辑都写在一个节点里。我一般会封装一个ArmController类,把 MoveIt2 的初始化、规划、执行、场景管理都封装进去,对外暴露简洁的接口。
class ArmController: def __init__(self, node, config): self.moveit2 = MoveIt2(...) self.config = config def move_joints(self, joints): return self.moveit2.move_to_configuration(joints) def move_pose(self, pose): return self.moveit2.move_to_pose(pose) def add_obstacle(self, name, position, size): self.moveit2.add_collision_box(name, position, [0,0,0,1], size)这样上层业务逻辑不需要关心 MoveIt2 的细节,代码可维护性会好很多。
8.2 配置文件分离
关节名、规划组名、速度参数这些不要硬编码在代码里,放到 YAML 配置文件里。换机械臂的时候只改配置不改代码。
arm_controller: joint_names: - shoulder_pan_joint - shoulder_lift_joint - elbow_joint - wrist_1_joint - wrist_2_joint - wrist_3_joint group_name: ur_manipulator base_link: base_link end_effector: tool0 max_velocity: 0.4 max_acceleration: 0.38.3 异步执行与状态反馈
move_to_configuration是阻塞的,执行期间节点无法处理其他事情。如果需要在运动过程中响应其他事件,要用异步方式:
import threading def async_move(self, joints): thread = threading.Thread(target=self.moveit2.move_to_configuration, args=(joints,)) thread.start() return thread但异步执行要注意线程安全,MoveIt2 的接口不是线程安全的,多个线程同时调用会出问题。建议加锁或者用队列串行化。
8.4 日志与监控
生产环境里日志很重要。我一般会在关键节点打日志:规划开始、规划成功/失败、执行开始、执行完成。日志里带上时间戳和关键参数,方便事后分析。
self.get_logger().info(f'Planning started at {time.time()}, target: {joints}') success = self.moveit2.move_to_configuration(joints) self.get_logger().info(f'Planning finished, success: {success}, duration: {time.time() - start}')监控方面,可以发布规划状态话题,让上层系统知道机械臂当前在做什么。这个在多机协作场景里特别重要。
8.5 单元测试与仿真测试
pymoveit2 的代码也可以写单元测试。用 pytest 加上 ROS 2 的 launch_testing,可以在仿真环境里自动化测试规划逻辑。
我一般会写几类测试:初始化测试、关节空间规划测试、笛卡尔规划测试、避障测试、异常处理测试。每次改代码后跑一遍,确保没有回归。
仿真测试用 Gazebo 或者 Ignition,加载机械臂模型和 MoveIt2 配置,跑完整的任务流程。仿真测试通过后再上实机,能避免很多低级错误。
代码组织这块没有标准答案,关键是让代码可读、可维护、可测试。我见过把几千行规划逻辑写在一个文件里的项目,后期维护成本极高。从一开始就做好模块化,后面会省很多事。