目录
前言
这一章学习了话题的订阅和发布,即话题通信。在我的理解中,机器人需要实时不断的处理传感器传来的数据并处理,进而做出相应的动作,这个过程并不是静态的。所以我们在运行不同功能包的节点时,有时候需要在各个节点中不断进行通信,ros因此引入了一套订阅发布机制:话题通信。
一、话题通信介绍
ros2的话题通信机制包含四个要素,发布者、订阅者、话题名称和消息接口(话题类型)。我们在终端中运行打开海龟模拟器,再打开另一个终端,运行命令:
$ rqt
在弹出界面中选择插件中的topic里面的topic monitor ,画面如图所示:

如图所示最左边就是这个节点相关的话题,type就是话题类型(topic_type),也就是消息接口(interface),展开每个话题还可以看到这个话题的消息接口定义的内容:

这里面的/turtle1/cmd_vel定义了linear(线速度)和angular (角速度)
或者我们打开海龟的键盘控制界面,再在另一个终端中输入命令:
$ ros2 run rqt_graph rqt_graph
就会打开一个图形界面 :

可以看到两个节点通过话题进行通信的结构图,上面两个是turtlesim节点发布了feedback和status话题,这上面显示的是话题名称,而turtlesim节点是这两个话题的发布者(publisher),teleop_turtle是这两个话题的订阅者(subscriber)。
二、X-terminal的几个快捷键
正式开始之前先记几个X-terminal的快捷键,比较方便好用:
| 命令名 | 作用 |
| 水平分割终端 | Shift+Ctrl+O |
| 垂直分割终端 | Shift+Ctrl+E |
| 关闭终端 | Shift+Ctrl+W |
| 新建标签页 | Shift+Ctrl+T |
| 切换到下一个标签页 | Ctrl+Page Down |
| 切换到上一个标签页 | Ctrl+Page Up |
三、话题通信实践——系统状态监测(根据鱼香ros大佬教程)
1.明确项目要求
这个项目的要求是通过一个小工具查看系统的实时状态,包括记录信息的时间、主机名称、CPU使用率、内存使用率、内存总大小、剩余内存、网络接收数据量和网络发送数据量,然后用一个图形界面显示出来。
我们基本可以明确要做的事就是:获取系统状态信息并通过话题发布出来,然后在另一个节点中订阅这个话题并进行显示。
我们首先创建自己的工作空间,通过以下命令:
~$ mkdir -p topic_practice_ws/src
~/topic_practice_ws$ colcon build
然后在工作空间根目录下输入code ./ 打开vscode工作区(这指令挺好用)
2.第一步:自定义通信接口
首先创建功能包status_interfaces:
~/topic_practice_ws/src$ ros2 pkg create status_interfaces --build-type ament_cmake --dependencies rosidl_default_generators builtin_interfaces --license Apache-2.0
builtin_interfaces:ros2中的一个消息接口功能包,可以使用其时间接口Time,表示记录信息的时间。
rosidl_default_generators:用于将自定义的消息文件转换为C++、python源码的模块。
然后创建话题消息定义文件,可以删除功能包里的src文件夹,也可以留着,然后在功能包里创建msg文件夹存放自定义文件,运行以下bash命令:
$ cd ~/topic_practice_ws/src/status_interfaces
~/topic_practice_ws/src/status_interfaces$ mkdir msg
~/topic_practice_ws/src/status_interfaces$ cd msg
~/topic_practice_ws/src/status_interfaces/msg$ touch SystemStatus.msg
这样就是在~/topic_practice_ws/src/status_interfaces/msg文件夹下创建SystemStatus.msg文件,这个文件只能在功能包的msg文件夹里,并且文件名必须以大写字母开头且只能由大小写字母组成。然后在 SystemStatus.msg文件中编写话题消息接口定义:
builtin_interfaces/Time stamp #记录时间戳
string host_name #系统名称
float32 cpu_percent #CPU使用率
float32 memory_percent #内存使用率
float32 memory_total #内存总量
float32 memory_available #剩余有效内存
float64 net_sent #网络发送数据总量
float64 net_recv #网络接收数据总量
随后在CMakeLists.txt中声明消息接口文件,在其中添加代码,因为消息接口定义中使用了builtin_interfaces这个消息接口功能包,所以别忘了声明它的依赖:
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/SystemStatus.msg"
DEPENDENCIES builtin_interfaces
)
然后在package.xml中添加消息接口的声明,添加以下代码:
<member_of_group>rosidl_interface_packages</member_of_group>
声明一下环境变量,运行以下代码验证消息接口文件是否已经被成功添加:
$ source install/setup.sh
$ ros2 interface show status_interfaces/msg/SystemStatus
这环境变量似乎是每次新打开终端都要重新声明一下,告诉ros2运行路径,否则就会出现Unknown package 'status_interfaces'这种找不到功能包的情况,运行结果如下:
builtin_interfaces/Time stamp #记录时间戳
int32 sec
uint32 nanosec
string host_name #系统名称
float32 cpu_percent #CPU使用率
float32 memory_percent #内存使用率
float32 memory_total #内存总量
float32 memory_available #剩余有效内存
float64 net_sent #网络发送数据总量
float64 net_recv #网络接收数据总量
这样消息接口文件就算自定义成功了。
3. 第二步:定义发布者publisher
创建一个功能包status_publisher编写发布者的代码,现在终端输入以下代码:
~/topic_practice_ws/src$ ros2 pkg create status_publisher --build-type ament-python --dependencies rclpy status_interfaces --license Apache-2.0
在功能包的status_publisher/status_publisher路径下新建文件sys_status_pub.py编写如下代码:
import rclpy
from rclpy.node import Node
from status_interfaces.msg import SystemStatus # 导入消息接口
import psutil # python库,用来获取CPU内存以及网络信息
import platform # python库,可以获取当前主机名称
class SysStatusPub(Node): #继承Node节点
def __init__(self, node_name):
super().__init__(node_name)
#创建发布者status_publisher,参数是(消息接口, 话题名称, 历史队列长度)
self.status_publisher_ = self.create_publisher(SystemStatus, 'sys_status', 10)
#创建定时器timer,每隔一秒调用一次timer_callback发布消息
self.timer = self.create_timer(1, self.timer_callback)
def timer_callback(self):
cpu_percent = psutil.cpu_percent()
memory_info = psutil.virtual_memory()
net_io_counters = psutil.net_io_counters()
msg = SystemStatus()
#get_clock().now()是从node节点继承来的方法,用来获得当前节点时钟时间,并通过to_msg()转换成builtin_interfaces.msg.Time消息复制给对象stamp
msg.stamp = self.get_clock().now().to_msg()
msg.host_name = platform.node()
msg.cpu_percent = cpu_percent
msg.memory_percent = memory_info.percent
#由于默认的数据单位都是字节,所以需要除以两次1024装换成MB
msg.memory_total = memory_info.total / 1024 / 1024
msg.memory_available = memory_info.available / 1024 / 1024
msg.net_sent = net_io_counters.bytes_sent / 1024 / 1024
msg.net_recv = net_io_counters.bytes_recv / 1024 / 1024
#调用publish函数把话题发布出去
self.get_logger().info(f'发布:{str(msg)}')
self.status_publisher_.publish(msg)
def main():
rclpy.init()
node = SysStatusPub('sys_status_pub')
rclpy.spin(node)
rclpy.shutdown()
然后要在setup.py中声明这个话题节点:
entry_points={
'console_scripts': [
'sys_status_pub = status_publisher.sys_status_pub:main',
],
},
然后编译并声明环境变量:
这里一定要注意!!!
colcon build 命令要在工作空间根目录下运行,也就是~/topic_practice_ws这个路径下运行,然后声明环境变量,也就是source install/setup.sh这个命令也要在工作空间根目录运行,要不然还是会出现找不到包Package 'status_publisher' not found.
再一个就是一个偶然的报错我发现报错信息说的运行的python版本和我虚拟环境的python版本不同,这才发现由于ROS 2的节点在编译时是由colcon工具处理的,而colcon在编译过程中使用的解释器是写死为系统环境的Python。这意味着,即使我在虚拟环境中编译了节点,运行时还是会使用系统环境的Python解释器,因此虚拟环境中安装的第三方依赖可能不会被正常使用。所以我放弃了使用虚拟环境。
最后运行节点:
~/topic_practice_ws$ ros2 run status_publisher sys_status_pub
效果如下:

这样就成功发布话题了。
4.第三步:创建订阅者subscriber并用Qt工具显示
还是先创建一个功能包status_display,并添加status_interfaces消息接口作为依赖:
~/topic_practice_ws$ ros2 pkg create status_display --build-type ament_cmake --dependencies rclcpp status_interfaces --license Apache-2.0
然后在 status_display/src目录下创建sys_status_display.cpp,编写如下代码:
#include <QApplication> //提供了Qt应用类QApplication
#include <QLabel> //Qt中用于显示文本的组件
#include <QString> //用于存储字符串
#include "rclcpp/rclcpp.hpp"
#include "status_interfaces/msg/system_status.hpp" //引入消息接口
//使用using关键字把消息接口替换成SystemStatus,简化代码
using SystemStatus = status_interfaces::msg::SystemStatus;
//定义SysStatusDisplay类,继承rclcpp的node节点并将这个节点命名为sys_status_display
class SysStatusDisplay : public rclcpp::Node {
public:
SysStatusDisplay() : Node("sys_status_display") {
//创建一个订阅者subscription_,<>里面是消息接口,传入参数为(话题名称, 历史队列长度, 回调函数)
//这里的回调函数是一个lambda表达式,&符号表示可以通过引用的方式直接捕获外部变量
subscription_ = this->create_subscription<SystemStatus>(
"sys_status", 10, [&](const SystemStatus::SharedPtr msg) -> void {
label_->setText(get_qstr_from_msg(msg));
});
// 初始化label_
label_ = new QLabel(get_qstr_from_msg(std::make_shared<SystemStatus>()));
label_->show();
}
//定义消息接口的共享指针命名为msg,通过get_qstr_from_msg转化为QString类型,然后调用std::stringstream显示文本
QString get_qstr_from_msg(const SystemStatus::SharedPtr msg) {
std::stringstream show_str;
show_str
<< "===========系统状态可视化显示工具============\n"
<< "数 据 时 间:\t" << msg->stamp.sec << "\ts\n"
<< "用 户 名:\t" << msg->host_name << "\t\n"
<< "CPU使用率:\t" << msg->cpu_percent << "\t%\n"
<< "内存使用率:\t" << msg->memory_percent << "\t%\n"
<< "内存总大小:\t" << msg->memory_total << "\tMB\n"
<< "剩余有效内存:\t" << msg->memory_available << "\tMB\n"
<< "网络发送量:\t" << msg->net_sent << "\tMB\n"
<< "网络接收量:\t" << msg->net_recv << "\tMB\n"
<< "==========================================";
//通过成员方法str()转化为std::string格式,然后给到QString::fromStdString函数生成一个QString类型
return QString::fromStdString(show_str.str());
}
private:
rclcpp::Subscription<SystemStatus>::SharedPtr subscription_;
QLabel* label_;
};
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
QApplication app(argc, argv);
auto node = std::make_shared<SysStatusDisplay>();
//由于rclcpp::spin和app.exec()都会阻碍程序运行,所以只能使用多线程来保证程序的正常运行
std::thread spin_thread([&]() -> void { rclcpp::spin(node); });
spin_thread.detach(); //当一个线程被detach后,它可以独立于主线程和其它线程运行。这意味着即使创建它的线程已经结束,被detach的线程仍然会继续执行,直到其任务完成
app.exec();
rclcpp::shutdown();
return 0;
}
在CMakeLists.txt中添加以下代码:
find_package(Qt5 REQUIRED COMPONENTS Widgets)
add_executable(sys_status_display src/sys_status_display.cpp)
target_link_libraries(sys_status_display Qt5::Widgets) #对于非ros功能包使用Cmake原生指令进行链接库
ament_target_dependencies(sys_status_display rclcpp status_interfaces)
install(TARGETS
sys_status_display
DESTINATION lib/${PROJECT_NAME})
构建后运行该节点或许没有数据,这是因为没有启动发布者,也就没有话题可订阅,所以我们需要在两个终端中一个启动发布者节点,一个启动订阅者节点,运行结果如下:
如此这个项目就圆满完成了。
3130

被折叠的 条评论
为什么被折叠?



