技术栈:ROS2 + Gazebo + Cartographer + Nav2
目标:应用搭建好的平台,跑通:建图-->定位-->自主导航-->自动探索的完整链路。
(1)ROS2 的最小世界观:
ROS2中的三个重要名词:Node(节点)、Topic(话题)和Message(消息)
Node表示一个独立运行的程序;Topic表示节点间传输数据的频道,类似于公司的广播,节点和节点的通信依靠同一个话题;Message表示频道里传递的数据格式,就类似于公司广播中传递的消息。
ros2 topic list,就可以看到当前的所有话题;ros2 topic echo,就可以找到需要的某个topic。
节点之间能够互相找到是因为它们底层使用的是DDS的通信机制:类似于广播的形式,一个节点发送信号,任何节点都可以看到,但是是否接收是要看他们之间的Topic是否一致,如果一个节点看到了和自己一样的Topic,就会接受信息,区别于Ros1中的TCP通信模式。在DDS机制中,每一个节点的启动,都会创建一下4个组件:
a. Domain的创建,只有处于同一个域的节点才可以相互通信,目标是确保不同项目之间可以隔离;
b. DomainParticipant, 类似于你的微信账号,每个节点都需要有一个自己的ID;
c. DataWriter,类似于你在微信群里面发消息,这个的作用是负责将特定类型的数据写入;
d. DataReader,类似于你在群里看到消息的动作,作用是负责从数据流中读取特定的类型的数据。
那么,节点之间是怎么进行通信的?
核心机制一:自动发现(通过topic)
举一个例子:一个talker的节点启动,Datawriter会通过组播(UDP)在局域网内进行广播:"大家好,我是Domain 0的成员,我能够发布名叫Chatter的Topic"
接着进行监听,当listener节点启动的时候,DataReader也在广播:"大叫好,我想订阅名叫Chatter的Topic"
内置的发现协议(SPDP/SEDP)会让这两者相互匹配成功,它们能够直接建立点对点的UDP(或者TCP)通道,然后开始传输数据,不用经过类似于ROS1中的Master。
核心机制二:QoS------服务质量策略
Reliability(可靠性),默认是设置为Reliability,如果数据没有接收到,中途丢了,就会重新发送,直到被接受;另一个是BEST-EFFORT,就是数据丢了也不管,比如激光雷达的点云数据;
Durability(持久性),默认是新接入的节点看不到前面的信息,如果设置为TRANSIENT-LOCAL,就会将最后的数据存放,新的订阅者可以接收到之前的历史数据;
Lifespan(寿命),看消息过了多久就作废,比如创干起的数据超过0.5s没有更新,就自动丢弃,防止机器人使用过时的信息。
DDS 在这个项目中的应用:
由于我是在VNC上运行机器人,因此Gazebo就相当于Datawriter,来实时传递仿真的激光雷达数据和里程计数据到数据流;启动的Cartpgrapher就相当于DataReader,来实时的接收这些数据;启动的RViz也是相当于Datareader,来实时接收这个仿真数据,然后将仿真结果绘制出对应的点云图像。
(2)机器人包含的传感器

(3)Cartographer SLAM
项目中应用到了Cartographer 算法来解决"我在哪里+我的周围是怎么样的",通过边猜边修正来构建地图,为一下4步骤循环:
a. 拿到激光扫描(/scan)+ 里程计(/odom)
b. 扫描匹配:把当前激光轮廓和已有小地图对齐(算一个最优位姿)
c. 把小地图(submap)和上一时刻的大地图进行拼接
d. 回环检测:走到之前来过的地方时,查看是否有和记忆对应的点
e. 全局修正所有位置
- TD坐标树
ROS2里面的所有位置关系都是使用TF来管理:

- 遥控操控
本质是"读取键盘的按键" + "转为/cmd_vel"的消息来发送,本项目中使用的消息结构是:

- 重要的仿真代码中的机器人"身体说明书"
第一部分:机器人的"身体"
base_link表示车身,也就是整台机器人的原点,所有其他部件(雷达、轮子、摄像头等)的位置,都是相对于这个base_link算出来的;在机器人的<link>的标签下,一般会包含三样东西:
(1)<inertial>,即这个机器人的"底子":
<inertial>
<mass>1.37290696e+00</mass> 表示这个机器人的重量是1.37kg。Gazebo会根据这个质量来计算重力、摩擦力和撞倒墙壁的反弹力。

着一部分表示惯性张量,即"转起来多费力",其中z轴的惯性张量最大,因为TurtleBot3这类机器人是扁平的方形,质量分布在四个角上,质量不是集中在中间,就类似于人将手举起合并转圈需要的力度小,但是张开双臂时,需要的力度就大一样。
(2)<collisoon> ,表示物理的骨架

表示这个物体的长宽高分别为26.5cm、26.5cm和8.9cm。有了这个物理量,可以计算物体是否撞倒障碍物之类的。
(3)<visual>,表示机器人的外在"衣服"

第二部分:激光雷达(SLAM的"眼睛")

包含有:使用GPU/CPU进行激光雷达的仿真,激光雷达扫描的频率,平均0.1s扫射一圈,还包含发布的Topic、数据所属的坐标系以及扫射一圈需要采样的点数和角度以及测距的范围,并添加上高斯噪声,模拟真实的传感器。
第三部分:IMU 传感器(用于测量线加速度和角速度)

IMU的测量频率是200Hz,发布的Topic为IMU,并模拟真实的传感器添加上高斯噪声,由于这包含有线性的加速度测量和角速度测量,所以分别在这两个测量的维度上添加上高斯噪声。
第四部分:轮子和关节(差速运动的基础)

先设定好TF父子关系, 接着设定关节的转动是<0 0 1>,绕着z轴转动,车轮一般是绕着x/y轴转动;effort=20,表示电机的最大扭矩是20N.m
第五部分:驱动插件

这部分是Gazebo差速驱动插件的配置,是让机器人在仿真环境下动起来的关键。
<plugin name="diff_drive" filename="libgazebo_ros_diff_drive.so">,表示让Gazebo加载一个名为"差速驱动"的动态库文件,目标是处理左右轮的差速转动;
<ros><remapping>cmd_vel:=/cmd_vel</remapping></ros>,设置这个插件的监听通道,话题为"/cmd_vel";
<left_joint>wheel_left_joint</left_joint>、<right_joint>wheel_right_joint</right_joint>,表示将参数"left_joint"的数值,等同于"wheel_left_joint"(仿真世界中的左轮子的关节名),也就是操控"left_joint",可以控制仿真世界中的左轮子的运动,右边也是如此;
<wheel_separation>0.287</wheel_separation>,表示左右轮子中心线的距离;<wheel_radius>0.033</wheel_radius>,表示车轮的实际半径,目标是使用里程计来计算机器人走了多少m,比如转了多少圈,圈数量*周长可以得到直线运动的距离,如果有转弯,也可以转动的角度;
最后一个部分:遇到的问题和解决:
- 当启动rviz后,运行Gazebo中的仿真机器人,但是Rviz中并不会实时显示激光雷达的点云数据,并且对应的终端显示"Message Filter dropping message: frame 'base_scan' at time 994.784 for reason 'discarding message because the queue is full'"
原因分析:由于有多个gzclient进程的残留,抢占CPU的渲染资源,导致gzserver被拖慢
解决方案:将残留的进程彻底清理:/usr/local/bin/ros_clean.sh,确认输出为0。
- 还会出现Queue waiting for data: (0, scan)
原因分析:也是由于多个gzclient进程的残留,导致CPU占据过多,数据流不连贯,Cartographer在等待数据。
解决方案:将残留的进程彻底清理:/usr/local/bin/ros_clean.sh,确认输出为0。