ros学习#5 话题订阅和发布

目录

前言

一、话题通信介绍

二、X-terminal的几个快捷键

三、话题通信实践——系统状态监测(根据鱼香ros大佬教程)

1.明确项目要求

2.第一步:自定义通信接口

3. 第二步:定义发布者publisher

4.第三步:创建订阅者subscriber并用Qt工具显示

前言

        这一章学习了话题的订阅和发布,即话题通信。在我的理解中,机器人需要实时不断的处理传感器传来的数据并处理,进而做出相应的动作,这个过程并不是静态的。所以我们在运行不同功能包的节点时,有时候需要在各个节点中不断进行通信,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})

       构建后运行该节点或许没有数据,这是因为没有启动发布者,也就没有话题可订阅,所以我们需要在两个终端中一个启动发布者节点,一个启动订阅者节点,运行结果如下:

 

如此这个项目就圆满完成了。 

 

 

 

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值