简介:一套基于ROS2与Gazebo的多机器人协同导航与动态编队仿真平台,面向机器人研究人员与工程师,用于在室内外复杂环境中验证自主路径规划、避障、队形保持及分布式控制算法。压缩包共70个文件、约1.2MB,内含18个launch启动文件、7个world场景文件、5个xacro模型、7个yaml参数、5个cpp源码、3个rviz配置等,覆盖从场景构建、模型加载、导航参数设定到算法运行与可视化的完整流程。平台集成ares_description、ares_navigation、ares_gazebo等模块,支持自定义控制算法与编队策略,可灵活调节动态障碍环境并实时观察队形变化,帮助研究者快速迭代验证,尤其适合动态障碍物环境下的编队切换与避障协同研究。目前已有85人学习下载,既适合开展分布式机器人系统控制算法与编队策略的验证,也可作为ROS2与Gazebo联合仿真的进阶学习样例。
1. 基于 ROS2 与 Gazebo 的多机器人协同导航与动态编队仿真平台:这套项目到底在解决什么问题
拿到一个叫“基于ROS2与Gazebo的多机器人协同导航与动态编队仿真平台”的压缩包,先别急着解压跑 demo。这类平台本质上是把三件事揉在一起:ROS2 做通信和算法组织,Gazebo 提供复杂室内外环境和传感器仿真,多台机器人则负责演示“自主路径规划 + 避障 + 队形保持”这一整条链路。它面向的不是某一个机器人,而是一组机器人同时运行时的协同问题——谁规划路径、谁避障、队形怎么不散、动态障碍物来了怎么临时变形。适合的人群也很明确:做多机器人或分布式系统方向的学生、研究控制算法需要快速验证的工程师,以及想把仿真结果迁移到真实小车上的硬件玩家。搞清楚它的架构和常见坑,比跑通 demo 更重要。
2. 为什么这套平台锁定 ROS2 和 Gazebo:仿真器与通信栈的选型逻辑
2.1 ROS2 的 DDS 通信和命名空间:多机器人“共用一套代码”的前提
多机器人仿真最怕的不是机器人多,而是话题和坐标系统一冲突。ROS1 时代靠 master 集中管理,一旦一台机器人的节点异常退出,整个系统都可能被拖垮。ROS2 改用 DDS 作为底层通信,节点之间通过域(domain)和话题发现机制直连,没有单点依赖,天然适合分布式机器人系统。对一个仿真平台来说,这意味着你可以同时拉起十台机器人,每台机器人各自跑导航和编队程序,互不干扰。
ROS2 的命名空间机制是支撑多机器人的关键。假设你写了一个差速小车的机器人描述和导航配置,想让三台车同时跑,最省事的做法是给每台车分配一个命名空间/robot1、/robot2、/robot3,再通过话题重映射把cmd_vel、odom、scan这些话题变成/robot1/cmd_vel、/robot1/odom。这里有个容易被忽略的细节:话题可以重映射,tf 树里的坐标系也必须加前缀。常见做法是把 robot_state_publisher 的frame_prefix参数设成robot1/,否则三台车会同时往同一个/tf话题上写数据,rviz2 里根本分不清谁是谁。
实际开发时,我还习惯把 ROS2 的 QoS 策略一并纳入设计。实时性高的传感器数据如激光雷达点云,用SensorDataQoS;控制指令cmd_vel用SystemDefaultQoS或BestEffort;而地图这类需要保证完整性的数据,则必须用Reliable。多机器人场景里最典型的翻车现场,就是某个节点默认是 volatile 的 best-effort,而另一个节点用 reliable 订阅,两边虽然能发现话题,但数据始终不刷新。这些参数在单机仿真里不敏感,一旦多机器人同时跑,问题会被迅速放大。
还需要提一句当前主流的运行环境。Ubuntu 22.04 对应 ROS2 Humble,这是目前多机器人仿真项目最常用的组合;ROS2 Humble 配 Gazebo Classic 11,功能最全。如果你想把仿真环境做成可复用的 Docker 容器,我一般会把 ros2 humble 和 gazebo 装在同一镜像里,然后用 gzserver 的无界面模式跑,这样既方便 CI 自动化跑批,也避免桌面渲染占用资源。
2.2 Gazebo 与其他机器人仿真平台对比:物理、传感器、动态障碍物哪个更顺
选择 Gazebo 而不是 Webots、CoppeliaSim,不是因为 Gazebo 更“高级”,而是因为它和 ROS2 的集成最直接。Gazebo Classic 通过 gazebo_ros_pkgs 提供 ROS2 接口,激光雷达、IMU、相机、轮式差速驱动都有现成插件。你在 URDF 里给底盘加上<gazebo>标签,指定插件,Gazebo 启动后会自动发布odom、scan等话题,nav2 导航栈直接订阅就能用,几乎不需要写中间转换层。
物理引擎方面,Gazebo 默认 ODE,支持切换 Bullet 和 DART。做编队控制和多机器人协同,ODE 够用,但要留意它默认的摩擦力和接触参数在坡道、楼梯场景下的表现。复杂室内外环境里,如果机器人频繁打滑或爬坡抖动,我会优先检查轮子与地面的摩擦系数,而不是改控制器。室内外混合环境的建模也比 Webots 方便:房屋墙体直接用 building 模型或静态 SDF 模型,室外地形用 heightmap,动态障碍物用 actor 走过场或者用脚本控制模型位置。
对比之下,CoppeliaSim 的脚本和 API 很灵活,但免费版在传感器仿真密度上受限;Webots 上手快,适合单机器人教学,但多机器人大规模场景的分布式节点部署不如 Gazebo 自然。做这套多机器人协同导航与动态编队仿真平台,核心诉求是“多机同时仿真 + 障碍物动态变化 + 与导航算法直接对接”,目前 Gazebo 是投入产出比最高的选择。
2.3 一套典型平台的目录结构与节点拓扑:从 world 到 control 的链路
这类压缩包里的项目目录通常有相对固定的套路,我自己组织代码时也沿用类似的划分方式。一般会有四个部分:worlds存放 Gazebo 世界文件,models或urdf存放机器人模型,nav存放 navigation2 参数和地图,src下是编队控制和节点启动逻辑。压缩包如果打包完整,还应该带一个launch目录,统一拉起整个仿真。
节点拓扑角度,一套最小可运行的多机器人协同仿真至少包含这么几层:
- Gazebo 层:gzserver 加载世界文件,spawn 多个机器人模型,发布传感器和里程计数据。
- 机器人模型层:每个机器人的 robot_state_publisher 发布 tf,joint_state_publisher 配合控制器插件发布关节状态。
- 导航层:每台机器人一套 navigation2 节点,完成全局规划、局部规划、costmap 更新。
- 编队层:一个或多个编队控制器节点,订阅各机器人位姿,计算出队形目标点后下发
cmd_vel给对应机器人。 - 人机交互层:rviz2 做可视化,必要时加 rqt_graph 看节点连接状态。
这套链路里,最容易出问题的连接点是 Gazebo 插件向 nav2 提供的数据格式。比如激光雷达插件默认发布的可能是sensor_msgs/LaserScan,而局部 costmap 期望看到的是有frame_id的 scan;如果 sensor frame 写错,costmap 会把所有障碍物当成在机器人坐标系原点,导航直接失效。这类问题不算硬性 bug,而是配置一致性,多机器人场景下配置翻倍,排查成本也跟着翻倍。
3. 搭建多机器人仿真环境:URDF 实例化、world 动态障碍与 launch 一键启动
3.1 用 Xacro 参数化机器人模型,避免多台车模型互相覆盖
写多机器人仿真时,最忌讳的是把一台机器人的 URDF 照着复制三份,然后手动改每一处 link 名字。正确做法是用 Xacro 宏定义机器人底盘,把prefix作为参数传入,之后让 launch 文件循环调用。这样机器人模型定义只有一份,却可以生成robot1_base_link、robot2_base_link等带前缀的独立坐标系。
一个典型的差速小车 Xacro 宏可以这样写:
<xacro:macro name="diff_drive_robot" params="prefix"> <link name="${prefix}_base_footprint"/> <link name="${prefix}_base_link"/> <link name="${prefix}_laser_link"/> <joint name="${prefix}_base_footprint_joint" type="fixed"> <parent link="${prefix}_base_footprint"/> <child link="${prefix}_base_link"/> <origin xyz="0 0 0.05"/> </joint> <joint name="${prefix}_laser_joint" type="fixed"> <parent link="${prefix}_base_link"/> <child link="${prefix}_laser_link"/> <origin xyz="0.15 0 0.1"/> </joint> <gazebo reference="${prefix}_base_link"> <material>Gazebo/White</material> <plugin filename="libgazebo_ros_diff_drive.so" name="${prefix}_diff_drive"> <ros> <namespace>/${prefix}</namespace> <remapping>odom:=odometry</remapping> </ros> <left_joint>${prefix}_left_wheel_joint</left_joint> <right_joint>${prefix}_right_wheel_joint</right_joint> <wheel_separation>0.35</wheel_separation> <wheel_radius>0.10</wheel_radius> <max_wheel_torque>20</max_wheel_torque> <command_topic>cmd_vel</command_topic> </plugin> </gazebo> <gazebo reference="${prefix}_laser_link"> <sensor name="${prefix}_laser" type="gpu_ray"> <pose>0 0 0 0 0 0</pose> <ray> <scan><horizontal><samples>360</samples><resolution>1</resolution></horizontal></scan> <range><min>0.12</min><max>10.0</max></range> </ray> <plugin filename="libgazebo_ros_ray_sensor.so" name="${prefix}_laser_scan"> <ros> <namespace>/${prefix}</namespace> <remapping>scan:=scan</remapping> </ros> <output_type>sensor_msgs/LaserScan</output_type> </plugin> </sensor> </gazebo> </xacro:macro>这里的逻辑很直接:宏定义了底盘、激光雷达和两个固定关节,同时在<gazebo>标签里挂上差速驱动和激光雷达插件。关键参数是namespace,它把插件发布的话题限定在各自命名空间下;command_topic指定差速插件订阅cmd_vel,之后编队控制器通过/robot1/cmd_vel就能控制对应底盘。wheel_separation和wheel_radius必须和实际模型尺寸一致,否则 Gazebo 里的 odom 会和真实位移产生偏差,导致后续导航和编队误差都被放大。
3.2 在 Gazebo 里布置室内外环境和动态障碍物:world 文件与 actor 脚本
多机器人协同导航要验证“复杂室内外环境”,所以 world 文件不能只是一块平地。我会把地图划分成三个区域:一个室内房间区,墙体用静态模型或 SDF 地标表示;一个开阔室外区,用 heightmap 做不平整地形;中间留一个连接走廊,布置动态障碍物。这样每台机器人可以在不同区域测试不同策略。
world 文件中,动态障碍物常用 Gazebo actor 实现。actor 的典型配置是这样:
<actor name="moving_pedestrian"> <pose>5.0 2.0 0 0 0 0</pose> <skin> <filename>model://pedestrian/mesh.dae</filename> </skin> <animation name="walk"> <filename>model://pedestrian/walk.dae</filename> </animation> <script> <loop_animation>true</loop_animation> <trajectory id="0"> <waypoint> <time>0</time> <pose>5.0 2.0 0 0 0 -1.57</pose> </waypoint> <waypoint> <time>5</time> <pose>8.0 2.0 0 0 0 1.57</pose> </waypoint> <waypoint> <time>10</time> <pose>5.0 2.0 0 0 0 -1.57</pose> </waypoint> </trajectory> </script> </actor>actor 在 Gazebo 里是一条预设轨迹的动画实体,会自己往前走,也能被激光雷达扫到,正好用来模拟动态行人。它的骨架是<skin>里的网格和<animation>里的动画文件,轨迹点用<waypoint>定义。仿真平台里如果自带行人模型最好,没有的话用简单 mesh 替代也完全可以。值得注意的是,actor 不会参与物理碰撞计算,如果编队算法需要把它当作真实碰撞体来测试,还得额外加一个碰撞模型,或在 costmap 中把它标记为静态障碍。
3.3 用 Python launch 循环拉起多个机器人:命名空间、spawn 与参数传递
模型定义好了,接下来是把多台机器人送进 Gazebo 并启动各自的话题。我习惯用 Python launch 文件循环生成节点,而不是手动重复写几十行。一个能扩展的 launch 骨架如下:
import os from launch import LaunchDescription from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node def generate_launch_description(): robot_num = LaunchConfiguration("robot_num", default="3") use_sim_time = LaunchConfiguration("use_sim_time", default="true") nodes = [] for i in range(1, int(robot_num.perform(None)) + 1): ns = f"/robot{i}" robot_xacro = os.path.join( get_package_share_directory("multi_robot_sim"), "urdf", "diff_drive_robot.xacro" ) robot_state_publisher = Node( package="robot_state_publisher", namespace=ns, executable="robot_state_publisher", parameters=[{ "robot_description": Command([ "xacro ", robot_xacro, " prefix:=", ns.lstrip("/") ]), "frame_prefix": ns + "/", "use_sim_time": use_sim_time, }], remappings=[ ("/tf", ns + "/tf"), ("/tf_static", ns + "/tf_static") ] ) spawn_entity = Node( package="gazebo_ros", executable="spawn_entity.py", arguments=[ "-entity", f"robot{i}", "-topic", ns + "/robot_description", "-x", str(-1.0 + i * 1.5), "-y", "0.0", "-z", "0.1" ] ) nodes.append(robot_state_publisher) nodes.append(spawn_entity) return LaunchDescription(nodes)这段代码的关键点有三个:命名空间、frame_prefix、tf 重映射。namespace=ns让节点的话题变成/robot1/...;frame_prefix给 tf 坐标系加前缀;remappings把默认的全局/tf和/tf_static重映射到各自命名空间下,避免多台机器人争抢同一个 tf 话题。实际运行时,还可以给 robot_state_publisher 加上use_sim_time参数,确保它使用 Gazebo 发布的时间,避免把系统时间混进来。启动之后,用ros2 node list和ros2 topic list检查,应该能看到带robot1_前缀的节点和话题。
4. 多机器人自主导航的实现:ROS2 Navigation2 的多机器人配置与动态避障避坑
4.1 Nav2 多机器人迁移要点:frame、topic 重映射和 costmap 的命名空间隔离
Navigation2(以下简称 Nav2)是 ROS2 下最常用的导航栈,功能覆盖全局规划、局部规划、costmap 和 lifecycle 管理。单机器人跑 Nav2 很容易,但多机器人同时跑时,前文提过的命名空间问题会再次出现。每一台机器人的 navigation2 节点都必须被限定在各自的命名空间里运行,并且它订阅的/scan、发布的/cmd_vel都要是对应机器人自己的版本。
一个常见的做法是给每个机器人单独启动一组 nav2 节点,而不是共享一份配置。在 launch 文件里,需要把 nav2 默认的 remapping 指到对应的 namespace。例如,把cmd_vel重映射为robot1/cmd_vel、scan重映射为robot1/scan、odom重映射为robot1/odom。如果一台机器人用了frame_prefix,tf 树中的坐标系也带了前缀,那么 Nav2 参数里的global_frame和robot_base_frame仍然可以写成map和base_link,前提是 tf 查询能够命中带前缀的坐标。这个细节不同项目写法不一样,最稳妥的办法是启动后用ros2 run tf2_ros tf2_echo查看坐标帧是否存在。
Nav2 的 costmap 是另一个关键隔离点。全局 costmap 和局部 costmap 的 frame 以及传感器 topic 必须指向本机。如果三台机器人共用同一个/scan,那么机器人 B 会把机器人 A 的位置当成障碍物,规划器会陷入“我方机器人也是障碍”的死循环。常见的配置片段如下:
global_costmap: global_frame: map robot_base_frame: base_link update_frequency: 2.0 publish_frequency: 1.0 plugins: - {name: static_layer, type: "nav2_costmap_2d::StaticLayer"} - {name: obstacle_layer, type: "nav2_costmap_2d::ObstacleLayer"} - {name: inflation_layer, type: "nav2_costmap_2d::InflationLayer"} obstacle_layer: enabled: true observation_sources: scan scan: topic: /robot1/scan sensor_frame: robot1/laser_link data_type: LaserScan marking: true clearing: true这段配置说明,obstacle layer 从/robot1/scan读取数据,并把障碍物标到map系中。关键参数是sensor_frame,它必须和 Gazebo 里激光雷达的坐标系一致。如果写成laser_link而实际 frame 是robot1/laser_link,costmap 会在错误位置标记障碍,导航轨迹会莫名绕远甚至卡死。
4.2 全局路径规划与局部避障的组合怎么选:SmacPlanner 与 Regulated Pure Pursuit
Nav2 的规划器组合直接影响避障和队形保持的效果。全局规划器常见的是 NavFn 和 SmacPlanner。NavFn 是经典的 A* 类栅格规划,速度快,但生成的路径比较“硬”,转弯不够平滑;SmacPlannerHybrid 则考虑机器人运动学模型,输出的轨迹更接近差速或阿克曼底盘的实际可执行路径。
在多机器人协同场景里,我更推荐 SmacPlannerHybrid 做全局规划。原因很简单:编队运动中,局部避障会让机器人频繁调整航向,如果全局路径本身不平滑,局部控制器会持续修正,导致车体横向抖振,队形误差很难收敛。当然,SmacPlanner 依赖正确的 footprint,必须在参数里把机器人轮廓描述清楚。
局部规划器方面,DWA 和 Regulated Pure Pursuit 是比较常用的两种。DWA 通过采样速度空间来评估轨迹,在静态障碍较多的室内环境效果好,但在动态障碍物较多、速度变化剧烈的场景容易来回搜索,导致车速抖动。Regulated Pure Pursuit 则更稳定,它让机器人跟踪路径前方的一个点,加上曲率约束和障碍抑制,适合在开阔环境下保持队形。做动态编队时,我会用 Regulated Pure Pursuit,把max_angular_accel和max_robot_pose_search_dist调得保守些,避免局部控制器把编队指令“纠正”得过猛。
4.3 动态障碍物下让规划“活起来”:obstacle layer 的 source 与参数调整
动态避障依赖 costmap 更新的实时性。Gazebo 中的行人、移动小车等动态障碍物,每时每刻都会在激光雷达里产生新的一圈点云。如果 costmap 的更新频率太低,动态障碍物会被“画”成假静态区,机器人会绕行一个已经不存在的位置。我把obstacle_layer的update_frequency调到 5 Hz 以上,局部 costmap 的更新频率可以更高。
另一个重要参数是clearing。如果 obstacle layer 只marking不clearing,障碍物离开雷达视野后,旧障碍痕迹会一直留在 costmap 里,时间一长整个地图都被“污染”。动态环境里务必保证clearing: true。同时combination_method决定 obstacle layer 与 static layer 的合并方式,默认 0 是覆盖,1 是叠加,建议设成 0,保证动态障碍物不会被静态地图的未知区域覆盖掉。
多机器人场景还有一个容易被忽略的问题:编队中机器人互相靠近时,前车的激光雷达会把后车当作障碍。为了避免队形被破坏,可以在 costmap 的 obstacle layer 里把友方机器人发布的可通行区域做过滤,或者让编队控制器的目标点尽量避开前车的传感器视野。这个其实已经进入运动规划的研究范畴,平台本身提供的是可以反复修改和观测的环境,具体策略要各自动手调。
5. 避坑 / 常见问题 / 排查:多机器人协同仿真跑不起来的几个高频坑
5.1 现象:tf 树互相覆盖,机器人各自看到对方的坐标系
多机器人同时启动后,rviz2 里出现多个 base_link 和时间戳混乱,或者/tf话题里同时夹杂着 robot1 和 robot2 的数据,这种情况几乎都出在 robot_state_publisher 的 tf 发布没有隔离。单机仿真的 launch 不会配置 tf 重映射,所以多机复用时很容易照搬出问题。
原因:多个 robot_state_publisher 节点发布到同一个/tf话题,tf 树里同名坐标系相互覆盖。解决方法是按前文所示,给每个机器人设置namespace,并把/tf和/tf_static分别重映射到/<机器人名>/tf。同时要在 rviz2 的 Fixed Frame 里填写形如robot1/odom的坐标系,而不是全局odom。排查时用ros2 topic echo /robot1/tf查看数据帧,再用ros2 run tf2_ros tf2_echo robot1/odom robot1/base_link验证坐标变换是否连续。
5.2 现象:Gazebo 窗口黑屏或闪烁,虚拟机里仿真像幻灯片
Gazebo 在虚拟机里跑时最容易出现画面闪烁、渲染错乱或黑屏。很多人把问题归咎于电脑不行,实际上大多数是 OpenGL 渲染驱动和 Gazebo 的默认渲染引擎不兼容。
原因:虚拟机的 OpenGL 版本过低,或 Gazebo 默认调用了 GPU 渲染但驱动不认。解决办法:在启动 Gazebo 前设置环境变量LIBGL_ALWAYS_SOFTWARE=1,强制走软件渲染,黑屏和闪烁通常能缓解。如果只需要跑仿真数据而不需要看画面,用 gzserver 无界面模式,配合 launch 里的spawn_entity完成机器人加载,只在需要可视化时再启动 gzclient。无界面模式还能明显降低 CPU 开销,跑多机器人编队时建议默认使用。
5.3 现象:一开 Nav2 机器人原地转圈,报 transform timeout
启动导航后机器人原地打转,终端反复输出TF_OLD_DATA或 transform timeout。这类问题不是控制算法写错,而是时间和坐标系的配置不一致。
原因:Gazebo 使用仿真时间,而 Nav2 或 robot_state_publisher 节点没有开启use_sim_time,两边的 tf 时间戳对不上。解决:所有参与仿真的节点都要在参数里给use_sim_time: true,launch 里可以用 group 的方式集中设置,而不是每个节点手动加。如果只有部分节点开启,也会出现间歇性丢 tf。排查顺序是先看/clock话题是否存在,再逐个检查节点的参数是否生效。
5.4 现象:动态障碍物一进感知范围,整个编队急停抖振
动态障碍物出现后,机器人不是平滑避障,而是急停、转向、再急加速,整个队形像步进电机一样一顿一顿。这在局部规划参数过于激进时特别明显。
原因:costmap 更新频率不足,局部控制器对突然出现的障碍物反应滞后;或者Regulated Pure Pursuit的参数把速度限制得过于严格,导致每次检测到障碍就触发大角度修正。解决:把局部 costmap 的update_frequency调到 5 Hz 以上,同时检查激光雷达扫描频率是否够用;降低局部规划器的最大线速度和角速度,让机器人对动态障碍的反应更平滑。可以先用rviz2的 costmap 显示功能,观察障碍物进入 costmap 的延迟,如果延迟超过半秒,问题就在传感器或 costmap 更新频率上。
5.5 现象:多台车跑到同一点互相锁死,谁也过不去
两个机器人迎面相遇时,各自把对面当成障碍,同时停下,然后互相等待,陷入死锁。这在没有集中调度的分布式导航中很常见,不是 bug,而是策略缺陷。
原因:每台机器人只考虑自己的 costmap,不知道对方的意图。解决思路有两种:一是编队策略层面错开轨迹,给不同机器人分配不同优先级,比如接受侧方避让;二是在仿真平台里加一个简单的交通规则节点,检测机器人之间的最小距离,距离过近时主动让低优先级机器人降速。这个坑也说明,多机器人协同导航的真正难点不在单车算法,而在多车冲突消解,这套仿真平台的价值正是为了暴露和验证这类问题。
6. 验证与进阶:用 ros2 bag 回放数据,把仿真结果量化到能写进论文
仿真跑通不是终点,能证明“机器人编队在动态障碍物环境中保持了队形、完成了避障”才是终点。我习惯在每个实验场景跑完后,立刻用 ros2 bag 把全量数据记录下来:
ros2 bag record -a -o team_run_001回放时再用 ros2 bag play 把odom、scan、tf、cmd_vel重新打回系统,配合 rviz2 慢慢看每一帧的变化。光看 rviz2 还不够,为了量化队形保持效果,我会订阅各机器人位姿,自己写一个小脚本,计算相邻机器人的间距误差、航向偏差和目标点跟踪误差。把这些误差输出成曲线,就能直观评价编队策略在动态障碍物下的收敛程度。做实验记录时,给每一组参数起一个可读的文件名,并保存当时的导航参数和编队参数,这是少走弯路最重要的习惯。
进阶方向是把这套仿真平台迁移到真实硬件。常见做法是把编队控制器从仿真环境里解耦出来,它只订阅各机器人位姿并下发速度。通过 ROS2 串口桥接节点把cmd_vel话题接到 ESP32 小车底板上,陀螺仪和轮式编码器把真实位姿回报回来,仿真里验证过的队形权重和速度限制就能直接复用到实物。我总会提醒自己做一件事:仿真环境里的传感器噪声是理想化的,迁移到实物后,第一件事不是调算法,而是先把里程计噪声和通信延迟摸清楚。希望这些经验能帮到你少踩几个坑。
本文还有配套的精品资源,点击获取