古月居《Ros 入门21讲》第 17 ~ 21 讲 (ROS 系统重要组件和功能包) 踩坑记录和学习笔记
一、ROS 中的坐标系管理系统 —— TF 功能包
功能:满足对机器人中众多不同坐标系的管理和计算各个坐标系之间的位置关系的需求。默认会记录十秒以内机器人所有坐标系之间的位置关系。
实现方式:广播-监听机制 (不同于之前的话题和服务机制),在启动 rosmaster、tf 后,会自动地在后台维护一个 tf 树来以树形结构的方式保存所有的坐标系,通过查询这个树来得到任意两个坐标系之间的位置关系。
小海龟跟随实验:
1 | // 安装功能包 |
可以看到,在键盘控制一只小海龟运动的时候,另一只会跟着他跑,并且会“抄近道”。
下面通过一条命令,生成描述当前环境下所有坐标关系的 pdf 文件 (以后会经常用到)
1 | rosrun tf view_frames |
可以看到主文件夹多了 frames.pdf 文件,打开可以看到:
其中,world 坐标系是全局的坐标系,坐标原点是仿真器的左下角,包含 x 和 y 方向;turtle1 和 turtle2 是位于两只小海龟身上的坐标系。图中 turtle1 和 turtle2 与 world 之间的箭头连线表明这两个坐标系和 world 坐标系是建立起联系的,图中所示即为 tf 树。
还可以通过 tf_echo 指令获取指定两个坐标系的位置关系:
1 | rosrun tf tf_echo turtle1 turtle2 |
运行后,终端会不断跟踪两个坐标系的关系并打印出来,打印的 Translation 字段描述两个坐标系原点的相对位置 (即如何平移才能让两个坐标系的原点重合在一起),Rotation 字段描述两个坐标系在角度上的相对位置 (即如何旋转才能让两个坐标系的方向重合在一起)。Rotation 字段还用三种不同的方式描述角度的相对位置,一种是四元数 (x,y,z,w),另一种是弧度描述的 RPY (即围绕 x、y、z 轴需要的旋转角度,这里因为是平面的坐标,所以只有围绕 z 轴的旋转),还有一种是角度描述的 RPY。
还可以通过三维可视化平台 Rviz 来直观的看到坐标系之间的关系:
1 | rosrun rviz rviz -d `rospack find turtle_tf`/rviz/turtle_rviz.rviz |
打开 rviz 界面后,首先需要把 fixed frame 字段改成 world,表明固定的坐标系是 world 坐标系;然后点击左下角的 Add 按钮,选择 tf,就可以看到平台上多了两个小海龟的位置坐标。通过按键移动其中一只小海龟,可以看到 rviz 界面中的坐标系也在运动。
二、tf 坐标系广播与监听的编程实现
在 catkin_ws/src 目录打开终端,执行创建功能包
1 | catkin_create_pkg learning_tf roscpp rospy tf turtlesim |
2.1 实现一个 tf 广播器
- 定义TF广播器(TransformBroadcaster)
- 创建坐标变换值;
- 发布坐标变换(sendTransform
turtle_tf_broadcaster.cpp 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50/**
* 该例程产生tf数据,并计算、发布turtle2的速度指令
*/
std::string turtle_name;
void poseCallback(const turtlesim::PoseConstPtr& msg)
{
// 创建 tf 的广播器实例 br
static tf::TransformBroadcaster br;
// 初始化 tf 数据
tf::Transform transform;
transform.setOrigin( tf::Vector3(msg->x, msg->y, 0.0) ); // 设置平移参数
tf::Quaternion q;
q.setRPY(0, 0, msg->theta); // 设置旋转参数
transform.setRotation(q);
// 广播 world 与海龟坐标系之间的 tf 数据,让 ros 环境可以知道有这两个坐标系
// 广播出去之后,ros 的 tf tree 就会添加这两个坐标系和他们的位置关系
br.sendTransform(tf::StampedTransform(transform, ros::Time::now(), "world", turtle_name));
}
int main(int argc, char** argv)
{
// 初始化ROS节点
ros::init(argc, argv, "my_tf_broadcaster");
// 输入参数作为海龟的名字
if (argc != 2)
{
ROS_ERROR("need turtle name as argument");
return -1;
}
turtle_name = argv[1];
// 订阅海龟的位姿话题
ros::NodeHandle node;
ros::Subscriber sub = node.subscribe(turtle_name+"/pose", 10, &poseCallback);
// 循环等待回调函数
ros::spin();
return 0;
};
2.2 实现一个 tf 监听器
- 定义TF监听器;(TransformListener)
- 查找坐标变换;(waitForTransform、 lookupTransform)
turtle_tf_listener.cpp 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66/**
* 该例程监听tf数据,并计算、发布turtle2的速度指令
* twist 用于产生海龟运动的数据
* spawn 用于产生另一只海龟
*/
int main(int argc, char** argv)
{
// 初始化ROS节点
ros::init(argc, argv, "my_tf_listener");
// 创建节点句柄
ros::NodeHandle node;
// 通过 service 请求产生turtle2
ros::service::waitForService("/spawn");
ros::ServiceClient add_turtle = node.serviceClient<turtlesim::Spawn>("/spawn");
turtlesim::Spawn srv;
add_turtle.call(srv);
// 通过 topic 创建发布 turtle2 速度控制指令的发布者
ros::Publisher turtle_vel = node.advertise<geometry_msgs::Twist>("/turtle2/cmd_vel", 10);
// 创建tf的监听器
tf::TransformListener listener;
ros::Rate rate(10.0); // 设置 while 循环的频率
while (node.ok())
{
// 获取turtle1与turtle2坐标系之间的tf数据
tf::StampedTransform transform;
try
{
// 等到系统中存在 turtle2 和 turtle1 的时候,才会跳过这条语句,否则原地等待;
// Time(0) 表明查询当前的时间所对应的数据;Duration(3.0) 表明等待三秒后会视为等待超时;
listener.waitForTransform("/turtle2", "/turtle1", ros::Time(0), ros::Duration(3.0));
// 查询两个坐标系的位置关系,结果存储在 transform 这个变量中;
listener.lookupTransform("/turtle2", "/turtle1", ros::Time(0), transform);
}
catch (tf::TransformException &ex)
{
ROS_ERROR("%s",ex.what());
ros::Duration(1.0).sleep();
continue;
}
// 根据turtle1与turtle2坐标系之间的位置关系,发布turtle2的速度控制指令
// atan() 函数返回指定坐标点 (x,y) 与坐标原点的连线和 x 轴正方向的夹角的弧度值
// atan() 函数接受的参数列表就是 y 在 x 前面,所以这里并不是刻意颠倒了顺序
// pow() 函数是求指数的函数,返回 x 的 y 次方,这里是用来求坐标 (x,y) 与原点的距离
geometry_msgs::Twist vel_msg;
vel_msg.angular.z = 4.0 * atan2(transform.getOrigin().y(),
transform.getOrigin().x());
vel_msg.linear.x = 0.5 * sqrt(pow(transform.getOrigin().x(), 2) +
pow(transform.getOrigin().y(), 2));
turtle_vel.publish(vel_msg);
rate.sleep();
}
return 0;
};
2.3 编译并运行
CMakeLists.txt 的 build 添加:
1 | add_executable(turtle_tf_broadcaster src/turtle_tf_broadcaster.cpp) |
回到 catkin_ws 目录执行 catkin_make 编译;编译成功后,打开六个终端,依次在其中运行以下指令:
1 | roscore |
其中,第 3、4 条指令的 __name 称为重映射,如果不加这个重映射,这两条指令创建的 ros 节点的节点名是一样的,会产生冲突。重映射就是让这个节点创建出来的时候,使用我们在 __name 参数后面指定的节点名,这样就避免了节点名冲突。再后面的 /turtle1 和 /turtle2 是传给这两个节点的参数,我们在节点中获取这两个参数并将其作为两个小海龟的名字。
运行结果和小海龟跟随实验相同,所以不放图了。这里打开了很多个终端、运行了很多条指令,但是在小海龟跟随实验当中我们只运行了一条 launch 指令,下面就来看看 launch:
三、launch 启动文件的使用方法
Launch 文件:通过 XML 文件实现多节点的配置和启动,运行 Launch 文件的同时也会自动地检测并启动 ROS Master。
3.1 什么是 XML 文件
Def:XML 指可扩展标记语言,被设计用来传输和存储数据。
特点:
- 标记语言,类似 HTML
- XML 标签没有被预定义,需要自行定义,即通过 XML 可以发明自己的标签
- 具有自我描述性,即通过 XML 标签可以对特定的文本内容进行描述,比如:
1
2
3
4
5
6
7<note>
<to>George</to>
<from>John</from>
<heading>Reminder</heading>
<body>Don't forget the meeting!</body>
</note>
- XML 的设计宗旨是传输数据,而非显示数据
- XML 仅仅是加了描述的纯文本
XML 和 HTML 的区别在哪里?
XML 被设计为传输和存储数据,其焦点是数据的内容;HTML 被设计用来显示数据,其焦点是数据的外观。
XML 的作用是什么?
- XML 把数据从 HTML 分离:通过使用几行 JavaScript,你就可以读取一个外部 XML 文件,然后更新 HTML 中的数据内容。
- XML 简化数据共享:XML 数据以纯文本格式进行存储,因此提供了一种独立于软件和硬件的数据存储方法。
- XML 简化数据传输:通过 XML,可以在不兼容的系统之间轻松地交换数据。
具体 XML 的学习,可以参考 W3School 的教程:https://www.w3school.com.cn/xml/index.asp
3.2 Launch 文件语法
标签 | 作用 |
---|---|
<launch> |
launch 文件中的根元素采用 <launch> 标签定义,即最外面必须包裹一层 <launch> 标签。 |
<node> |
用来启动某个 ROS 节点。需要传入的属性主要有:pkg —— 节点所在的功能包名称,比如 “turtlesim”;name —— 节点运行时的名称,比如 “sim1”;type —— 节点的可执行文件名称,还有 output 、respawn 、required 、ns 、args 等属性。 |
<param> / <rosparam> |
设置ROS系统运行中的参数,存储在参数服务器中。<param> 将某个参数保存在参数服务器中,如 <param name="output_frame" value="odom"/> ,其中 name 是参数名,value 是参数值;<rosparam> 是将参数文件中的所有参数都保存在参数服务器中,如:<rosparam file="params.yaml" command="load" ns=“params"/> |
<arg> |
设置仅限于 launch 文件使用的内部局部变量。定义方式:<arg name="arg-name" default="arg-value"/> ,使用方式:<param name="foo" value="$(arg arg-name)"/> |
<remap> |
重映射 ROS 计算图资源的命名,注意是重命名,原来的名字换掉以后就没有了。示例:<remap from="/turtlebot/cmd_vel" to="/cmd_vel"/> 。具体内容后面会讲。 |
<include> |
包含其他 launch 文件。示例:<include file="$(dirname)/other.launch" /> |
更多标签内容详见:http://wiki.ros.org/roslaunch/XML
3.3 Launch 示例
创建功能包:在 catkin_ws/src 目录下执行 catkin_create_pkg 命令,注意 Launch 文件是不需要依赖包的。
1 | catkin_create_pkg learning_launch |
创建后,进入 learning_launch 目录,创建 launch 目录,这里就存放所有的 launch 文件。在里面创建并编辑如下文件:
1 | <launch> |
这里 output 属性是设置节点的日志文件打印在终端屏幕上,如果不设定的话默认是不会显示在终端上的。
下面在根目录 catkin_ws 执行 catkin_make 编译。编译完成后启动 launch 文件:
1 | roslaunch learning_launch simple.launch |
启动以后就可以看到终端不断有日志输出出来了。并且两个节点发布的信息是同时显示在这一个终端上的,说明这个终端同时运行了两个节点,且他们是并行运行的。
3.3 Launch 命名空间
看下面这一段代码:
1 | <launch> |
在 <rosparam>
标签中的 file 属性使用了一个系统命令 “find”,其作用为让系统自动寻找 learning_launch 这个功能包,并输出它的路径。
下面在 learning_launch 目录下创建 config 目录,创建 param.yaml 文件:
1 | A: 123 |
在这个 yaml 文件中,就使用了命名空间 group,在存入参数服务器的时候,就会在这个空间下的参数名前面加一个 ‘group/‘ 前缀。命名空间的目的还是为了避免参数名称出现冲突。
在终端执行 roslaunch 命令,再通过 rosparam list 命令查看已有的参数列表,如下所示:
可以看到,在 turtlesim_parameter_config.launch 文件中定义在 <node>
标签下的参数都带上了一个前缀,这个前缀就是 node 的 name 属性。说明嵌套在 <node>
标签内部的属性 (包括从外部 yaml 文件引入的) 都会自动地被放在以节点名为名称的命名空间下。
3.4 通过 remap 修改 topic 名称
看下面的 launch 文件:
1 | <launch> |
这里在 turtlesim_node 通过 remap 修改了现有的一个话题的名称,从 “/turtle1/cmd_vel” 改成了 “/cmd_vel”。这个改动可以通过运行 rostopic list 指令查看,可以看到话题列表中,这个话题的名称确实已经改动过了。但是这个改动并不会对在 <include>
标签中通过 simple.launch 引入并启动的节点产生影响,它们还是可以正常地用原来的话题名发布和接收消息的。
remap 的作用域是什么?当 remap 在 node 之外,它的作用域是他之后的所有节点;当 remap 被 node 包裹在其中。它的作用域是当前节点。
四、常用可视化工具的使用
- rqt,直接运行命令即可。单独运行 rqt 命令可以显示一个集成大部分功能的可视化工具。
- rviz,三维可视化工具、数据显示平台。可以看到机器人模型、坐标、运动规划、导航、点云、图像等。需要先运行 roscore,然后执行 rosrun rviz rviz
- Gazebo,三维物理仿真平台。在虚拟机可能运行不起来,因为硬件驱动能力有限、服务器在国外。可以下载离线模型来节省下载模型的时间。
五、课程总结与进阶攻略
- 功能包能够很快的实现某种效果,但是优化还是需要理解功能包的实现机制和相关的专业知识。
- 深入理解功能包的前提是深入理解了机器人学的基本理论,可以访问下面的链接:
斯坦福大学公开课 —— 机器人学 https://www.bilibili.com/video/av4506104/
交通大学 —— 机器人学 https://www.bilibili.com/video/av18516816/?p=2
资料整理:
- ROS : https://www.ros.org
- ROS Wiki : http://wiki.ros.org/
- ROSCon 2012 ~ 2019 : https://roscon.ros.org
- ROS Robots : https://robots.ros.org/
- Ubuntu Wiki : https://wiki.ubuntu.org.cn
- 古月居 :http://www.gyh.ai
- zhangrelay的专栏 :https://blog.csdn.net/ZhangRelay
- 易科机器人实验室:http://blog.exbot.net/
- 开源机器人学学习指南:https://github.com/qqfly/how-to-learn-robotics
六、参考文献
Ros 中 Remap(话题重映射)的两种使用方法:https://blog.csdn.net/qq_23670601/article/details/88529739
roslaunch/XML/remap:http://wiki.ros.org/roslaunch/XML/remap
XML 教程:https://www.w3school.com.cn/xml/index.asp