上次我们学习了 TF 的基本概念和如何发布静态的 TF 坐标:

这次来总结下如何发布一个自定义的 TF 坐标转换,并监听这个变换。

一、编写 TF 广播者

进入上次创建的 learning_tf2 包中:

roscd learning_tf2

src 下新建一个 turtle_tf2_broadcaster.cpp 文件,代码如下:

#include <ros/ros.h>

// 存储要发布的坐标变换
#include <geometry_msgs/TransformStamped.h> // 四元数
#include <tf2/LinearMath/Quaternion.h> // 变换广播者
#include <tf2_ros/transform_broadcaster.h> // 乌龟的位姿定义
#include <turtlesim/Pose.h> std::string turtle_name; void poseCallback(const turtlesim::PoseConstPtr& msg)
{
// 创建 tf 广播对象
static tf2_ros::TransformBroadcaster br; // 存储要发布的坐标变换消息
geometry_msgs::TransformStamped transformStamped; // 变换的时间戳
transformStamped.header.stamp = ros::Time::now(); // 父坐标系名称
transformStamped.header.frame_id = "world"; // 当前要发布的坐标系名称 - 乌龟的名字
transformStamped.child_frame_id = turtle_name; // 乌龟在二维平面运动,所以 z 坐标高度为 0
transformStamped.transform.translation.x = msg->x;
transformStamped.transform.translation.y = msg->y;
transformStamped.transform.translation.z = 0.0; // 用四元数存储乌龟的旋转角
tf2::Quaternion q; // 因为乌龟在二维平面运动,只能绕 z 轴旋转,所以 x,y 轴的旋转量为 0
q.setRPY(0, 0, msg->theta); // 把四元数拷贝到要发布的坐标变换中
transformStamped.transform.rotation.x = q.x();
transformStamped.transform.rotation.y = q.y();
transformStamped.transform.rotation.z = q.z();
transformStamped.transform.rotation.w = q.w(); // 用 tf 广播者把订阅的乌龟位姿发布到 tf 中
br.sendTransform(transformStamped);
} int main(int argc, char** argv)
{
// 当前节点的名称
ros::init(argc, argv, "my_tf2_broadcaster");
ros::NodeHandle private_node("~"); // 判断当前要广播的乌龟节点名字
if (!private_node.hasParam("turtle")) {
// launch 文件和命令行都没有传递乌龟名称,就直接退出
if (argc != 2) {
ROS_ERROR("need turtle name as argument");
return -1;
}; // launch 文件中如果没有定义乌龟名称,就在命令行中加上
turtle_name = argv[1];
} else {
// 从 launch 文件获取乌龟名称参数
private_node.getParam("turtle", turtle_name);
} ros::NodeHandle node; // 订阅一个节点的 pose msg,在回调函数中广播订阅的位姿消息到 tf2 坐标系统中
// turtle_name 为 turtle1 时广播 turtle1 的位姿到 tf 中
// turtle_name 为 turtle2 时广播 turtle2 的位姿到 tf 中
ros::Subscriber sub = node.subscribe(turtle_name + "/pose", 10, &poseCallback); ros::spin();
return 0;
};

这个程序的意思是订阅输入乌龟的 pose 话题,然后在 poseCallback 回调函数中发布 world 到乌龟的 TF 变换,注意这个程序可以接收不同乌龟的 pose 消息,只要运行时指定乌龟的名称 turtle_name 即可,代码注释很详细,其他的就不说了,然后添加编译规则:

add_executable(turtle_tf2_broadcaster src/turtle_tf2_broadcaster.cpp)
target_link_libraries(turtle_tf2_broadcaster ${catkin_LIBRARIES})

直接编译:

catkin_make

基本上不会出问题,为了方便启动我们在 launch 文件中启动广播者:

<launch>
<!-- 乌龟节点 -->
<node pkg="turtlesim" type="turtlesim_node" name="sim"/> <!-- 控制乌龟运动的键盘节点 -->
<node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/> <!-- 线速度和角速度的定义,但是在这个例子中并没有用到哎... -->
<param name="scale_linear" value="2" type="double"/>
<param name="scale_angular" value="2" type="double"/> <!-- 第一个乌龟的 tf 广播者节点,参数为乌龟 1 的名字 /tutle1 -->
<node pkg="learning_tf2" type="turtle_tf2_broadcaster" args="/turtle1" name="turtle1_tf2_broadcaster" /> <!-- 第二个乌龟的 tf 广播者节点,还是用相同的节点,只不过改变了传递的参数为乌龟 2 的名字 /turtle2 -->
<node pkg="learning_tf2" type="turtle_tf2_broadcaster" args="/turtle2" name="turtle2_tf2_broadcaster" /> </launch>

然后就可以直接启动了:

roslaunch learning_tf2 start_demo.launch

为了确定是否成功广播了变换,使用下面的命令查看一个变换的输出:

rosrun tf tf_echo /world /turtle1

如果在控制台输出类似下面的消息,则说明变换发布成功:

下面我们来编写一个 TF 接收者来使用我们上面发布的变换。

二、编写 TF 接收者

同样在 src 目录下创建 turtle_tf2_listener.cpp,代码如下:

#include <ros/ros.h>

// 接受 tf 变换
#include <tf2_ros/transform_listener.h> // 转换消息
#include <geometry_msgs/TransformStamped.h> // 发布到乌龟 2 的运动消息:角速度和线速度
#include <geometry_msgs/Twist.h> // 再生服务
#include <turtlesim/Spawn.h> // 实现乌龟 2 跟随乌龟 1 运动
int main(int argc, char** argv)
{
// 当前节点的名字
ros::init(argc, argv, "my_tf2_listener"); ros::NodeHandle node;
ros::service::waitForService("spawn");
ros::ServiceClient spawner = node.serviceClient<turtlesim::Spawn>("spawn"); turtlesim::Spawn turtle; turtle.request.x = 4;
turtle.request.y = 2;
turtle.request.theta = 0;
turtle.request.name = "turtle2";
spawner.call(turtle); // 角速度和线速度消息发布者,用来发布计算后的新的速度消息
ros::Publisher turtle_vel = node.advertise<geometry_msgs::Twist>("turtle2/cmd_vel", 10); // tf 变换缓存区,最多缓存 10 秒
tf2_ros::Buffer tfBuffer; // 创建监听 tf 变换对象,创建完毕即开始监听,通常定义为成员变量
tf2_ros::TransformListener tfListener(tfBuffer); ros::Rate rate(10.0);
while (node.ok()) {
// 用来保存寻找的坐标变换
geometry_msgs::TransformStamped transformStamped;
try{
// 寻找坐标变换
transformStamped = tfBuffer.lookupTransform("turtle2", "turtle1", ros::Time(0));
}
catch (tf2::TransformException &ex) {
ROS_WARN("%s",ex.what());
ros::Duration(1.0).sleep();
continue;
} // 用来保存角速度和线速度
geometry_msgs::Twist vel_msg; // 新的角速度为寻找到的变换角速度的 4 倍 - 使得第二个乌龟的运动轨迹转弯更快,且轨迹是弧线
vel_msg.angular.z = 4.0 * atan2(transformStamped.transform.translation.y, transformStamped.transform.translation.x); // 新的线速度是寻找到的变换线速度的 0.5 倍 - 使得第二个乌龟的运动速度为第一个乌龟的一半
vel_msg.linear.x = 0.5 * sqrt(pow(transformStamped.transform.translation.x, 2) + pow(transformStamped.transform.translation.y, 2)); // 发布新的速度消息,乌龟 2 节点的内部订阅了这个消息,所以乌龟 2 会收到新的角速度和线速度,以此产生跟随运动
turtle_vel.publish(vel_msg); rate.sleep();
} return 0;
};

这里关键的代码如下:

// 保存寻找的变换
geometry_msgs::TransformStamped transformStamped; // 寻找 turtle1 到 turtle2 的坐标变换
// target_frame: turtle2
// source_frame: turtle1
// ros::Time(0): 获取变换的时间,
transformStamped = tfBuffer.lookupTransform("turtle2", "turtle1", ros::Time(0));

同样添加编译规则:

add_executable(turtle_tf2_listener src/turtle_tf2_listener.cpp)
target_link_libraries(turtle_tf2_listener ${catkin_LIBRARIES})

然后编译:

catkin_make

在上面广播者的 launch 文件中加上接收者的启动:

<!--
这个例子一共创建了 5 个节点:
1. 乌龟节点,包含 2 个小乌龟
2. 控制乌龟运动的键盘节点
3. 第一个乌龟的 tf 广播者节点
4. 第二个乌龟的 tf 广播者节点
5. tf 坐标系统的监听节点,用来监听 2 个乌龟之间的坐标变换
-->
<launch>
<!-- 乌龟节点,这个节点的内部应该是创建了 2 个乌龟...... -->
<node pkg="turtlesim" type="turtlesim_node" name="sim"/> <!-- 控制乌龟运动的键盘节点 -->
<node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/> <!-- 线速度和角速度的定义,但是在这个例子中并没有用到哎... -->
<param name="scale_linear" value="2" type="double"/>
<param name="scale_angular" value="2" type="double"/> <!-- 第一个乌龟的 tf 广播者节点,参数为乌龟 1 的名字 /tutle1 -->
<node pkg="learning_tf2" type="turtle_tf2_broadcaster" args="/turtle1" name="turtle1_tf2_broadcaster" /> <!-- 第二个乌龟的 tf 广播者节点,还是用相同的节点,只不过改变了传递的参数为乌龟 2 的名字 /turtle2 -->
<node pkg="learning_tf2" type="turtle_tf2_broadcaster" args="/turtle2" name="turtle2_tf2_broadcaster" /> <!-- 启动 tf 坐标系同的监听节点 -->
<node pkg="learning_tf2" type="turtle_tf2_listener" name="listener" /> </launch>

然后启动:

roslaunch learning_tf2 start_demo.launch

运行时会出现 2 个小乌龟,把窗口焦点放到终端,按上下左右键会发现第二个乌龟跟随第一个乌龟运动:

但是刚启动时终端会报个错误:

[ERROR] [1418082761.220546623]: "turtle2" passed to lookupTransform argument target_frame does not exist.
[ERROR] [1418082761.320422000]: "turtle2" passed to lookupTransform argument target_frame does not exist.

这是因为我们在 turtle2 还没有产生之前就寻找变换,导致没有找到它,为了解决这个问题可以在寻找变换前等待变换可用:

// 第四个参数是阻塞等待的超时时间
listener.waitForTransform("/turtle2", "/turtle1", ros::Time::now(), ros::Duration(3.0)); transformStamped = tfBuffer.lookupTransform("turtle2", "turtle1", ros::Time(0));

加上这句运行时就不会报错了,今天就写到这里,下次见:)

ROS 机器人技术 - 广播与接收 TF 坐标的更多相关文章

  1. ROS 机器人技术 - 解决 ROS_INFO 不能正确输出 string 的问题!

    一.输出「??」 项目调试一个节点,打印 ROS 信息时发现设置的节点名称都是问号: ROS_INFO("[%s]: camera_extrinsic_mat", kNodeNam ...

  2. ros机器人开发概述

    1.       ROS项目开发流程? 参照古月大神写的ROS探索总结系列:http://blog.exbot.net/archives/619 具体项目设计可看看<程序员>杂志的最新一篇 ...

  3. ROS机器人程序设计(原书第2版)补充资料 (柒) 第七章 3D建模与仿真 urdf Gazebo V-Rep Webots Morse

    ROS机器人程序设计(原书第2版)补充资料 (柒) 第七章 3D建模与仿真 urdf Gazebo V-Rep Webots Morse 书中,大部分出现hydro的地方,直接替换为indigo或ja ...

  4. ROS机器人操作系统相关书籍、资料和学习路径

    作者:Top Liu链接:https://zhuanlan.zhihu.com/p/30391098来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 本文是易科机器人实验 ...

  5. ROS机器人程序设计(原书第2版)补充资料 教学大纲

    ROS机器人程序设计(原书第2版) 补充资料 教学大纲 针对该书稍后会补充教学大纲.教案.多媒体课件以及练习题等. <ROS机器人程序设计>课程简介 课程编号:XXXXXX 课程名称:RO ...

  6. ROS机器人程序设计-学习小结-

    ROS官网 |易科 |虞坤林 |古月居 |ROSClub 学习ROS相关书籍推荐:http://blog.csdn.net/zhangrelay/article/details/52244746 RO ...

  7. ROS机器人程序设计(原书第2版)补充资料 (拾) 第十章 使用MoveIt!

    ROS机器人程序设计(原书第2版)补充资料 (拾) 第十章 使用MoveIt! 书中,大部分出现hydro的地方,直接替换为indigo或jade或kinetic,即可在对应版本中使用. MoveIt ...

  8. ROS机器人程序设计(原书第2版)补充资料 (捌) 第八章 导航功能包集入门 navigation

    ROS机器人程序设计(原书第2版)补充资料 (捌) 第八章 导航功能包集入门 navigation 书中,大部分出现hydro的地方,直接替换为indigo或jade或kinetic,即可在对应版本中 ...

  9. ROS机器人程序设计(原书第2版)补充资料 (壹) 第一章 ROS系统入门

    ROS机器人程序设计(原书第2版)补充资料 (壹) 第一章 ROS系统入门 书中,大部分出现hydro的地方,直接替换为indigo或jade或kinetic,即可在对应版本中使用. 第一章主要包括R ...

随机推荐

  1. 《MapReduce: Simplified Data Processing on Large Clusters》论文研读

    MapReduce 论文研读 说明:本文为论文 <MapReduce: Simplified Data Processing on Large Clusters> 的个人理解,难免有理解不 ...

  2. Spring Boot中的事务是如何实现的

    本文首发于微信公众号[猿灯塔],转载引用请说明出处 今天呢!灯塔君跟大家讲: Spring Boot中的事务是如何实现的 1. 概述 一直在用SpringBoot中的@Transactional来做事 ...

  3. 重学 Java 设计模式:实战策略模式「模拟多种营销类型优惠券,折扣金额计算策略场景」

    作者:小傅哥 博客:https://bugstack.cn - 原创系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 文无第一,武无第二 不同方向但同样努力的人,都有自身的价值和亮 ...

  4. 基于SpringBoot AOP面向切面编程实现Redis分布式锁

    基于SpringBoot AOP面向切面编程实现Redis分布式锁 基于SpringBoot AOP面向切面编程实现Redis分布式锁 基于SpringBoot AOP面向切面编程实现Redis分布式 ...

  5. QQ群消息监听并将消息存储到SQLite数据库中

    目录 一.前言 二.效果图 1.插件界面 2.SQLite数据库 3.QQ群消息 三.准备工作 1.CQA软件 2.CQA-SDK易语言版本 3.易语言破解版 4.使用到的相关模块 四.开始撸代码 五 ...

  6. day65 django进阶(1)

    目录 一.聚合查询与分组查询 1 聚合查询(aggregate) 2 分组查询(annotate) 二.F与Q查询 1 F查询的三个功能 1.1 能帮助我们直接获取到表中某个字段对应的数据 1.2 获 ...

  7. (二)学习了解OrchardCore笔记——开篇:OrchardCore的中间件

    现在开始看Starpup的中间件.这是一个扩展方法app.UseOrchardCore() public void Configure(IApplicationBuilder app, IHostEn ...

  8. EOS基础全家桶(十五)智能合约进阶2

    简介 今天我们继续补充智能合约的进阶使用技巧,这次的主题是交易,合约内我们除了可以发起内联action的调用,很多使用还需要直接调用其他的合约action或者以交易的形式调用自身的action. 发起 ...

  9. Quartz.Net系列(十六):通过Plugins模式使用Xml方式配置Job和Trigger和自定义LogPrivider

    1.简单介绍 Quarz.Net中采用插件式来实现配置文件配置,通过XMLSchedulingDataProcessor类进行Xml数据处理 默认配置文件命名:quart_jobs.xml publi ...

  10. easyui datagrid 中添加combobox

    项目需要,如下图所示 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> &l ...