简介:基于ROS的流水线及机械臂联合仿真项目,结合MoveIt!与Gazebo完成轨迹规划与整线运行演示,面向机器人、自动化方向的本科及硕士教研人群。工程内同时提供AssemblyLine流水线与独立arm机械臂两套ROS工作空间,覆盖模型描述(xacro/urdf/stl)、Gazebo世界环境、MoveIt规划配置、C++控制节点与launch一键启动脚本,并附运行结果与README,便于对照复现和二次开发,也可作为课程设计或毕业设计的基础。整个zip包共81个文件,含22个launch、15个xml、8个yaml、7个cpp、6个xacro、7个stl、2个world和2个rviz等,将机器人建模、场景搭建、运动规划与仿真执行串联成完整闭环,体积仅3.81MB,目录层次简明、按模块分组。该项目已有317人学习/下载,特别适合正在入门MoveIt轨迹规划、Gazebo物理仿真或流水线上下料协作的开发者学习参考。
1. 一条“传送带 + 机械臂”的ROS仿真线:MoveIt!做规划、Gazebo做物理、运行结果能复现
机械臂在流水线上做轨迹规划,从仿真到实物最容易翻车的地方往往不是算法,而是“开关没对上”:MoveIt!算好了一条轨迹,RViz里拖得飞起,Gazebo里的模型却纹丝不动;或者模型动了,末端却一头扎进传送带底座的碰撞里。这条基于ROS的流水线及机械臂方案,核心就是把MoveIt!、Gazebo、ros_control和传送带模型按正确顺序装进同一套时钟和话题体系,让规划、执行、记录一次跑通。它适合正在做机械臂开发、准备上真实线体的工程师,也适合先用仿真验证轨迹规划算法的实验场景。下文按我平时搭这类方案的操作顺序来:先搭好Gazebo仿真环境,再做MoveIt!配置,解决RViz与Gazebo的同步,然后写抓取控制脚本,最后集中排一遍坑。
2. 搭Gazebo仿真环境:选机械臂模型、写URDF、用MoveIt!向导生成配置包
2.1 机械臂型号与Gazebo仿真环境模型怎么选
如果你是第一次在这个方向落地,我建议直接选 Ubuntu 20.04 + ROS Noetic + Gazebo 11 + MoveIt 1 这套组合。它的资料最多,控制器配置和action接口都稳定,很多坑都能在网上找到对应记录。ROS2 Humble + MoveIt2 也可以用,但话题命名、参数传递和action使用方式都有差异,等ROS1这条链路走通了再平移过去会省力得多。
机械臂我一般选 Franka Emika Panda,原因是它的URDF、SRDF和MoveIt官方示例都非常完整。做流水线项目时,Panda的7个自由度和夹爪正好够覆盖“抓取-搬运-放置”这类典型动作。至于“Gazebo仿真环境模型”,这个词在资源包里经常出现,实际就是指把机械臂的URDF/SDF和传送带模型一起spawn进同一个Gazebo世界。传送带在Gazebo里没有现成模型,常见做法是用一块带摩擦的平板加一个prismatic joint来模拟,工件放在平板上随关节移动,再触发一个到位信号。
# 先装基础依赖,确保moveit、gazebo_ros、ros_control齐全(Ubuntu 20.04 + ROS Noetic) sudo apt install ros-noetic-moveit ros-noetic-moveit-setup-assistant \ ros-noetic-gazebo-ros ros-noetic-gazebo-plugins \ ros-noetic-ros-control ros-noetic-ros-controllers # 创建流水线工作空间并编译验证 mkdir -p ~/pipeline_ws/src cd ~/pipeline_ws/src catkin_init_workspace cd ~/pipeline_ws catkin_make source devel/setup.bash这块的逻辑很简单:MoveIt、Gazebo和ros_control都是二进制安装,不要自己源码编译,否则会引入版本错位。catkin_make只编译你自己的描述包和脚本包。装完顺手跑一句rosversion gazebo_ros和rosversion moveit_core,确认版本不是你装错源或者多版本混在一起。ROS本身的手动配置源比较麻烦,用鱼香ROS的一键安装脚本能省不少时间,但装完之后同样要用上面两条命令核对版本。
2.2 把机械臂和流水线写进一份xacro
不要直接用URDF文件,而是用xacro。原因是流水线场景要复用:机械臂型号、传送带长度、工件尺寸这些参数都应该在xacro顶部定义,改一个变量就能适配不同线体。把Panda机械臂include进来,再叠加一张传送带平板,最后统一加<gazebo>插件块,这是最常见的组织方式。
<!-- robot.xacro 片段:引入Panda臂并给ros_control留出接口 --> <xacro:include filename="$(find panda_description)/urdf/panda.xacro"/> <gazebo> <plugin name="gazebo_ros_control" filename="libgazebo_ros_control.so"> <robotNamespace>/</robotNamespace> </plugin> </gazebo> <!-- 传送带:一块1.2m的平板,允许沿X轴滑行 --> <joint name="belt_joint" type="prismatic"> <origin xyz="0 0 0.3" rpy="0 0 0"/> <parent link="world"/> <child link="conveyor_belt"/> <axis xyz="1 0 0"/> <limit lower="-0.6" upper="0.6" effort="10" velocity="0.2"/> </joint>注意xacro里的$(find panda_description)必须能在你的ros包路径下找到,否则Gazebo启动时会因资源定位失败而停在“模型加载中”。这根传送带joint的关键参数是velocity和effort:velocity决定工件移动速度,effort是马达能施加的最大力,工件质量大了之后如果effort不够,传送带会被拖着走,轨迹规划里工件位置就会偏移。
2.3 用MoveIt! Setup Assistant生成专属配置包
环境模型写好后,不要手写MoveIt配置,直接用MoveIt Setup Assistant生成。它能自动做自碰撞矩阵、规划组、预定义位姿这些琐碎但关键的配置。手写最常漏掉的是“自身碰撞检查”,少了它MoveIt!规划出来的路径可能直接穿过机械臂自己的连杆。
# 启动MoveIt Setup Assistant,图形界面里加载robot.xacro source /opt/ros/noetic/setup.bash roslaunch moveit_setup_assistant setup_assistant.launch界面里加载转换后的URDF后,我会按这个顺序操作:建立规划组时,把panda_arm的7个关节放进一个组,夹爪单独建组;碰撞矩阵用默认采样密度即可,不要为了追求精细盲目提高采样密度,否则SRDF文件会膨胀到几百KB,规划查询变慢;预定义位姿里至少给一个“ready”和一个“home”,后续脚本里要用它们做起始复位。生成出来的配置包要放回~/pipeline_ws/src重新编译。之后每次改xacro,都要重新过一遍这个向导,否则碰撞检测用的还是旧模型。
3. MoveIt2 RViz与Gazebo同步:控制器、仿真时间和话题三层对表
3.1 ros_control控制器配置:JointTrajectoryController是MoveIt!的执行出口
很多人以为MoveIt!可以直接驱动Gazebo里的机械臂,这是误解。MoveIt!只负责规划,算出的轨迹通过action发给控制器,真正让Gazebo里连杆动起来的是ros_control里的JointTrajectoryController。我见过太多案例:RViz里MoveIt轨迹播放正常,Gazebo里纹丝不动,最后查出来是控制器根本没被加载。
arm_controller: type: position_controllers/JointTrajectoryController joints: - panda_joint1 - panda_joint2 - panda_joint3 - panda_joint4 - panda_joint5 - panda_joint6 - panda_joint7 state_publish_rate: 50 action_monitor_rate: 20 allow_partial_joints_goal: truestate_publish_rate是关节状态发布频率,50Hz已经足够流畅,太大会增加话题负载;action_monitor_rate是轮询action goal状态的频率,20Hz是常规值;allow_partial_joints_goal设为true,意思是允许MoveIt只控制panda_arm组而不去管夹爪,这样规划时更灵活。如果你的场景里用到了夹爪,可以在同一个控制器下把夹爪关节也加进去,或者单独再起一个夹爪控制器,不要混在一个trajectory controller里。
3.2 先查/clock再谈同步:use_sim_time必须全链路打开
RViz和Gazebo“不同步”的根源,几乎都是时间源不一致。Gazebo默认以仿真时间推进,一个step通常是10毫秒;而RViz、MoveIt如果仍然用系统墙钟时间,就会出现“MoveIt显示已经到位,Gazebo里的臂还没走完”的错位感。更隐蔽的是,时间不一致还会导致轨迹插值计算错误,终端里偶尔会冒出来“trajectory is not continuous”之类的警告。
# 确认Gazebo时间源是否接管全局(ROS1) rosparam get /use_sim_time rostopic hz /clock第一行返回true,说明系统已在使用仿真时间;第二行应该看到80~100Hz的/clock频率。如果/clock没有输出,多半是gazebo_ros的launch没有正确启动,或者你是在Gazebo启动前就打开了RViz,时间源没有来得及切换。ROS2下对应的是ros2 param get /use_sim_time和ros2 topic hz /clock,检查思路一样。我一般会在所有launch文件的<node>之前统一加一行<param name="/use_sim_time" value="true"/>,而不是只在一个节点里设置,防止某些节点后启动时没被覆盖。
3.3 RViz里拖拽末端,Gazebo不跟:把话题和action对上
RViz中的拖拽目标先发送给move_group节点,再由move_group以action goal的形式发给控制器。如果RViz里拖得动、Gazebo不动,重点怀疑两处:控制器没注册成功,或者move_group没认到控制器的action server。实际操作我会分三步定位。
# 查看控制器加载情况,state应为running rosservice call /controller_manager/list_controllers如果返回的arm_controller是 “not running”,用switch_controller把它启动;如果根本没有这个控制器条目,说明yaml配置没被加载。如果控制器是running但Gazebo还是不动,再执行rostopic echo /joint_trajectory_controller/state,看有没有持续的目标点更新。这一步可以把问题从“控制链路”和“MoveIt发现机制”至少割开一半。
| 检查项 | 命令/话题 | 正常表现 |
|---|---|---|
| 时间同步 | rostopic hz /clock | 80~100Hz |
| 控制器状态 | rosservice call /controller_manager/list_controllers | arm_controller state: running |
| 轨迹下发 | rostopic echo /joint_trajectory_controller/state | 有持续更新的目标位置 |
| MoveIt状态 | rostopic echo /move_group/status | 没有 rejected |
3.4 传送带上的工件位置怎么进规划场景
MoveIt!规划时不知道传送带上有什么东西,需要把工件事先加进PlanningScene。最简单的方式是把它当成一个box碰撞体,在程序里发布CollisionObject到/planning_scene话题。
from moveit_msgs.msg import CollisionObject from shape_msgs.msg import SolidPrimitive co = CollisionObject() co.header.frame_id = "world" co.id = "workpiece" co.operation = CollisionObject.ADD box = SolidPrimitive() box.type = SolidPrimitive.BOX box.dimensions = [0.08, 0.08, 0.05] co.primitives.append(box)注意这里用frame_id = "world",要和机械臂的base_link以及传送带在世界系下的位姿一致,否则MoveIt!会在规划时把工件放到错误位置,路径看着奇怪但说不清为什么。当工件随传送带移动后,不要只更新坐标,要先把旧CollisionObject REMOVE再ADD,否则PlanningScene里会同时存在新旧两个碰撞体,机械臂可能会绕开一个已经不存在的位置。
4. 流水线抓取的轨迹规划实现:写一个会等传送带的MoveIt Python脚本
4.1 用MoveIt Commander完成“落到工件上方”的最小可运行脚本
链路检查完就可以写控制脚本了。MoveIt的Python接口是moveit_commander,它把MoveGroupCommander封装得比较友好。我习惯让脚本先回到“ready”位姿,这一步既是任务开始,也是体检:如果回ready都不能完成,说明控制器同步或关节限位配置还挂着问题。
#!/usr/bin/env python3 import rospy import moveit_commander rospy.init_node("pick_from_conveyor") moveit_commander.roscpp_initialize([]) arm = moveit_commander.MoveGroupCommander("panda_arm") arm.set_planning_time(5.0) arm.set_goal_tolerance(0.01) arm.set_max_velocity_scaling_factor(0.4) arm.set_max_acceleration_scaling_factor(0.3) # 回到安全位姿,顺便验证move_group -> controller整条链路 arm.set_named_target("ready") arm.go(wait=True)set_goal_tolerance(0.01)是关节目标允许的偏差,单位是弧度;set_planning_time(5.0)限制单次规划最多5秒,超时返回失败;set_max_velocity_scaling_factor(0.4)把执行速度压到额定的40%,仿真里感觉不明显,但在真实线体上是防止急停冲击的第一道保护。新手常犯的错是把wait设为False,然后立刻执行下一个目标,轨迹还没来得及发出就被覆盖了,至少要等GoalStatus变成SUCCEEDED。
4.2 从等待工件到精准抓取:订阅话题 + 笛卡尔路径
流水线上不能假设工件永远在同一个位置,所以要用一个节点监听到位信号。常见做法是传送带的光电传感器(Gazebo里就是ray sensor)在工件到位后发布geometry_msgs/Pose,机械臂脚本收到后才开始规划抓取。
from geometry_msgs.msg import Pose from tf.transformations import quaternion_from_euler def wait_for_workpiece(timeout=30): # 流水线到位信号,由传送带节点发布 return rospy.wait_for_message("/workpiece_pose", Pose, timeout=timeout) wp = wait_for_workpiece() target = Pose() target.position.x = wp.position.x target.position.y = wp.position.y target.position.z = 0.35 q = quaternion_from_euler(0, 0, 1.5708) # 末端姿态转向来料方向 target.orientation.x, target.orientation.y, target.orientation.z, target.orientation.w = q waypoints = [arm.get_current_pose().pose, target] plan, fraction = arm.compute_cartesian_path(waypoints, eef_step=0.01, jump_threshold=0.0) if fraction < 1.0: rospy.logwarn("笛卡尔路径走不通,可达比例 %.2f,切换关节空间规划", fraction) arm.set_pose_target(target) plan = arm.plan() arm.execute(plan, wait=True)eef_step=0.01表示末端每0.01米插值一次,Panda精度足够;jump_threshold=0.0表示不因关节突变幅度截断路径。fraction是笛卡尔路径可达比例,小于1意味着中间某个点规划失败,不要忽略它直接执行,否则机械臂可能走到一半急停。这时我会打印出waypoints里的异常点,多半是工件坐标写错或者碰撞体没更新。
4.3 关节空间和笛卡尔路径怎么选:一张表说明白
| 路径类型 | 适用场景 | 优点 | 坑 |
|---|---|---|---|
| 关节空间规划 | 点到点搬运、避障 | 规划快、成功率高 | 末端实际走弧线,无法预测 |
| 笛卡尔路径 | 抓取、画线、装配 | 末端轨迹直线可控 | 容易因奇异点或碰撞失败 |
| 混合方式 | 流水线抓取 | 先直线接近再绕障 | 切换点容易有姿态突变 |
在流水线抓取里,我更倾向于“先笛卡尔、失败后回退关节空间”的混合策略,这也是4.2示例代码里的逻辑。但要注意:切换时目标用set_pose_target只约束了末端位置和姿态,不约束路径形状,所以末端可能在中间走一条略奇怪的大回环。如果线体周围有立柱或护栏,需要额外加约束路径,或者直接把关键waypoint写全。
4.4 运行结果记录:把规划时间和执行日志落盘
标题里的“运行结果”指的是规划后的轨迹数据。徒手截图没有说服力,我习惯把每次规划的耗时、轨迹点数、执行是否成功追加到一个CSV文件,同时用rosbag记录关节状态。
# 录制一次完整运行,回放时可以对比轨迹与关节反馈 rosbag record -O pipeline_run.bag /joint_states /tf /workpiece_poseimport time import rospy start = rospy.Time.now().to_sec() res = arm.plan() planning_time = rospy.Time.now().to_sec() - start n_points = len(res.trajectory.joint_trajectory.points) with open("run_result.csv", "a") as f: f.write(f"{planning_time:.3f},{n_points},{rospy.Time.now().to_sec():.3f}\n")轨迹点数反映路径复杂度和插值密度,点数异常变多时,通常意味机械臂在走一个非常绕的路径,需要回头检查障碍物或者目标位置。不同ROS版本下arm.plan()返回结构略有差异,第一次跑先打印一下res,确认字段名是trajectory.joint_trajectory.points还是直接trajectory.points,再写解析。
5. 轨迹规划仿真避坑:5个让MoveIt!和Gazebo翻车的常见问题
5.1 RViz轨迹动画正常,Gazebo里纹丝不动
现象:RViz里拖目标点,轨迹绿色线条正常生成,播放也正常,但Gazebo里的机械臂完全不响应。
原因:move_group把规划好的轨迹通过action发给了JointTrajectoryController,但控制器没有运行,或者move_group没发现控制器的action server。这个现象在误加载多个controller_manager时特别容易出现。
解决:先执行rosservice call /controller_manager/list_controllers确认arm_controller状态。如果显示不是running,用switch_controller启动它;如果列表里没有这个控制器,说明gazebo.launch里没有加载controller yaml。如果列表正常,检查/joint_trajectory_controller/state话题是否有数据。这三步能定位80%以上的执行端问题。
5.2 Gazebo里机械臂一启动就往下塌或关节乱颤
现象:模型spawn进Gazebo后,手臂在重力下快速下垂,关节角度乱飘,轨迹规划还没开始就已经像“散架”。
原因:URDF里link的inertial字段缺失或者质量为零,Gazebo动力学解算直接算出无穷大角速度。另一个常见原因是关节PID没给,位置控制器无法抵抗重力。
解决:逐个link检查<inertial>块,必须保证mass大于0且惯性张量不是对角线全0。如果你用的是effort_controllers/JointTrajectoryController,在控制器yaml里补上PID增益,例如panda_joint1: {p: 1000.0, i: 0.0, d: 10.0}。先用最小速度让机械臂在home位姿待住,确认关节不飘后再跑规划。
5.3 轨迹执行像慢动作,实际时间比规划时长多几倍
现象:MoveIt!规划显示2秒完成,Gazebo里实际磨蹭6秒,拖动视角看机械臂动作像降速。
原因:大部分是时间同步问题。MoveIt!按wall time推进轨迹,Gazebo按sim time推进,两边节奏不匹配,控制器为保证轨迹连续性只能放慢。另外state_publish_rate太低也会让轨迹插值看起来卡顿。
解决:先确认rosparam get /use_sim_time是true,再用rostopic hz /clock看频率,正常情况下应该在80Hz以上。RViz如果是在Gazebo完全启动后才打开的,最好重启一次,让它从头使用仿真时间。检查代码如下:
rosparam set /use_sim_time true rostopic hz /joint_states5.4 Gazebo加载模型卡死,日志停在同一行不往下走
现象:roslaunch一直停在spawn模型阶段,Gazebo界面里模型加载到一半就卡住,CPU占满,甚至直接无响应。
原因:URDF或SDF里引用了绝对路径的mesh文件,Gazebo用了在线模型库校验,或者资源路径里带了中文和空格。这三个原因的表现完全一样,不好区分。
解决:把机械臂所有mesh复制到自己包内的meshes目录里,xacro的mesh引用改成file://$(find my_robot_description)/meshes/xxx.stl,同时导出GAZEBO_MODEL_PATH指向本地模型目录,不让在线模型库参与加载。排查时用gzserver --verbose直接启动世界,能看得更细:
gzserver --verbose my_world.world5.5 多个节点抢发目标,后发的覆盖先发的
现象:轨迹脚本在跑,RViz里手动拖了一下目标点,脚本的当前目标立刻被取消;或者两个脚本同时发目标,执行结果随机。
原因:JointTrajectoryController的action默认是抢占策略,最后一个接收到的goal会顶掉前面的,MoveIt侧并不会自动做仲裁。
解决:不要让每个业务节点都直接调用move_group,而是让所有意图先发给一个仲裁节点,由仲裁节点决定哪个节点有执行权,再去调用move_group。这个设计在真实线体里尤其重要,因为急停、视觉触发、人工插手三个源会同时存在。仿真阶段至少要做到“脚本运行时别开RViz拖拽调试”。等以后做真实机械臂和流水线分布式部署时,还会遇到ROS多机通信配置的坑,最典型的是ROS_MASTER_URI指到了localhost;但在Gazebo单机仿真里,把master和gazebo放同一台机器,这类问题不会出现。
6. 把运行结果变成验收依据:批量跑、rosbag回放、再练强化学习
当单一一次抓取能稳定跑通时,我会把流程交给批量脚本,用多次冷启动的统计结果来验收。因为热重启会掩盖资源泄漏和环境残留,“第一次跑就成功”才有意义。下面这段脚本在流水线场景里很实用:
for i in $(seq 1 5); do roslaunch my_robot_gazebo full_pipeline.launch > /tmp/trial_$i.log 2>&1 & LAUNCH_PID=$! sleep 12 timeout 60 rosrun my_robot_scripts run_trial.py kill $LAUNCH_PID wait $LAUNCH_PID 2>/dev/null done5次冷启动后,把日志里的planning_time_ms和goal_status提取出来,算平均规划时间和成功率。如果某次明显偏长,打开对应日志看是规划失败还是执行超时。轨迹回放我习惯用rosbag play pipeline_run.bag --clock --loop,它可以对比同一段轨迹在修改URDF前后的差异,帮我判断改动是否引入了额外抖动。
再往后走,这个环境很适合接Gazebo强化学习:让MoveIt!负责上层规划,Gazebo返回末端位姿和接触信息,把“规划成功率+执行时间”当作奖励信号,训练一个能适应不同工件位置的策略。前提是每次环境复位必须彻底,工件初始位置、传送带速度、机械臂起始位姿全部写死,否则训练方差会大到没法收敛。我自己每次改完URDF,都会先把第3章的检查命令过一遍,再丢进批量脚本,不然心里没底。希望帮到你。
本文还有配套的精品资源,点击获取