话题通信实践案例

自定义通信接口

在 chapter_3 下创建一个新的文件层,接着在 src/ 下创建功能包,并进入功能包,在其目录下创建文件 msg,再进入 msg/,创建文件SystemStatus.msg

1
2
3
4
5
6
7
8
cd ~/learn_ros2/chapter_3/
mkdir -p topic_practice_ws/src
cd src/
ros2 pkg create status_interfaces --dependencies builtin_interfaces rosidl_default_generators --license Apache-2.0
cd status_interfaces/
mkdir msg
cd msg
touch SystemStatus.msg

编辑 .msg 文件内容

1
2
3
4
5
6
7
builtin_interfaces/Time stamp #记录时间戳
string host_name # 主机名字
float32 cpu_percent # cpu使用率
float32 memory_percent # 内存使用率
float32 memory_available # 内存总大小
float64 net_sent # 网络发送数据总量
float64 net_receive # 网络数据接收总量

在 CMakeLists 和 package.xml 文件中添加相应语句

1
2
3
4
5
6
# cmake函数,来自与rosidl_default_generators,将msg等消息接口定义文件转换成库或者头文件类
# 它会根据提供的消息接口去生成对应的cpp文件
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/SystemStatus.msg"
DEPENDENCIES builtin_interfaces
)
1
2
3
<license>Apache-2.0</license>
# 添加这句
<member_of_group>rosidl_interface_packages</member_of_group>

然后编译并修改环境变量

1
2
3
cd ~/learn_ros2/chapter_3/topic_practice_ws/
colcon build
source install/setup.bash

编译好之后,在 topic_practice_ws/installl 下,有一个与功能包同名的文件,在该文件下有一个 include 文件,该文件下也有一个与包名同名的文件,里面有个 msg文件,在该文件中,就是 .msg 文件转换得来的 .hpp 和 .h 文件

另外,在 detail 文件中,system_status__struct.hpp 文件里有 .msg文件中定义的属性

查看生成的头文件的定义

1
ros2 interface show status_interfaces/msg/SystemStatus

系统信息发布与获取

在 topic_practice_ws/src/ 下创建一个功能包 status_publisher,要依赖 status_interfaces,这里创建python的,因为要使用python的一个库 psutil,可以相对简单的得到系统当前的各种信息

1
ros2 pkg create status_publisher --build-type ament_python --dependencies rclpy status_interfaces --licence Apache-2.0

在 status_publisher/src 下创建文件 sys_status_pub.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
67
68
69
70
71
72
73
74
75
76
import rclpy  # 导入ROS 2的Python客户端库
from status_interfaces.msg import SystemStatus # 导入自定义消息类型 SystemStatus
from rclpy.node import Node # 导入ROS2节点类
import psutil # 用于获取系统的硬件状态,如CPU、内存和网络等
import platform # 用于获取系统的基本信息,如主机名

# 定义一个类 SysStatusPub,继承自 Node 类
class SysStatusPub(Node):
def __init__(self, node_name):
# 初始化节点,node_name是节点的名字
super().__init__(node_name)

# 创建一个发布器,用于发布类型为 SystemStatus 的消息到 'sys_status' 话题,队列大小为 10
self.status_publisher_ = self.create_publisher(SystemStatus, 'sys_status', 10)

# 创建定时器,每隔1秒调用一次 timer_callback 方法
self.timer_ = self.create_timer(1.0, self.timer_callback)

# 定时器回调函数,每隔1秒被调用一次
def timer_callback(self):
# 获取CPU的使用率,返回值为浮动百分比
cpu_percent = psutil.cpu_percent()

# 获取内存的使用信息,包括已使用、总量、可用等数据
memory_info = psutil.virtual_memory()

# 获取网络I/O的发送和接收信息(字节数)
net_io_counters = psutil.net_io_counters()

# 创建一个SystemStatus消息对象
msg = SystemStatus()

# 设置时间戳,获取当前时间并转换为消息格式
msg.stamp = self.get_clock().now().to_msg()

# 获取主机名
msg.host_name = platform.node()

# 获取并设置CPU使用率
msg.cpu_percent = cpu_percent

# 获取并设置内存的使用百分比
msg.memory_percent = memory_info.percent

# 获取并设置内存的总大小,转换为float类型
msg.memory_total = float(memory_info.total)

# 获取并设置内存的可用大小,转换为float类型
msg.memory_available = float(memory_info.available)

# 获取并设置网络发送的字节数,单位转换为MB
msg.net_sent = net_io_counters.bytes_sent / 1024 / 1024

# 获取并设置网络接收的字节数,单位转换为MB
msg.net_recv = net_io_counters.bytes_recv / 1024 / 1024

# 记录日志,输出当前的SystemStatus消息
self.get_logger().info(f'发布: {str(msg)}')

# 发布消息
self.status_publisher_.publish(msg)


# main函数,程序入口
def main():
# 初始化rclpy客户端库
rclpy.init()

# 创建一个SysStatusPub节点对象,节点名称为 'sys_status_pub'
node = SysStatusPub('sys_status_pub')

# 使节点进入循环,保持节点运行并监听定时器事件
rclpy.spin(node)

# 关闭节点并关闭rclpy
rclpy.shutdown()

在 setup.py 中添加相应语句

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
from setuptools import find_packages, setup

package_name = 'status_publisher'

setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='yanzu',
maintainer_email='yanzu@todo.todo',
description='TODO: Package description',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
# 添加这句
'sys_status_pub = status_publisher.sys_status_pub:main',
],
},
)

然后编译运行

1
2
3
colcon build
source install/setup.bash
ros2 run status_publisher sys_status_pub

打印接口信息

1
ros2 topic echo /sys_status

在功能包中使用QT

在 topic_practice_ws/src/ 下创建功能包 status_display,且要依赖 status_interfaces

1
ros2 pkg create status_display --dependencies rclcpp status_interfaces --license Apache-2.0

在 status_display/src/ 下创建文件 hello_qt.cpp,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <QApplication>
#include <QLabel>
#include <QString>


int main(int argc, char** argv){

QApplication app(argc, argv);
QLabel* label = new QLabel();
QString message = QString::fromStdString("hello qt!");
label->setText(message);
label->show();
app.exec(); // 执行应用,阻塞代码

return 0;
}

在 CMakeLists 中添加相应语句

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查找 qt5 组件
find_package(Qt5 REQUIRED COMPONENTS Widgets)

# 添加可执行文件
add_executable(hello_qt src/hello_qt.cpp)

# 添加依赖库
target_link_libraries(hello_qt Qt5::Widgets)

# 拷贝节点到 install/lib
install(TARGETS hello_qt
DESTINATION lib/${PROJECT_NAME}
)

然后编译执行

1
2
3
colcon build
source install/setup.bash
ros2 run status_display hello_qt

订阅数据并使用Qt显示

在 status_display/src/ 下创建文件 sys_status_display.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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <QApplication>      // 引入Qt应用程序库,用于创建GUI界面
#include <QLabel> // 引入Qt标签控件,用于显示系统状态信息
#include <QString> // 引入Qt字符串类,用于处理字符串
#include <rclcpp/rclcpp.hpp> // 引入ROS2 C++客户端库
#include "status_interfaces/msg/system_status.hpp" // 引入自定义消息类型 SystemStatus

// 定义 SystemStatus 类型简写
using SystemStatus = status_interfaces::msg::SystemStatus;

// 定义 SysStatusDisplay 类,继承自 ROS2 的 Node 类
class SysStatusDisplay : public rclcpp::Node {

private:
// 定义一个共享指针的订阅者,订阅 SystemStatus 类型的消息
rclcpp::Subscription<SystemStatus>::SharedPtr subscriber_;

// 定义 QLabel 控件,用于显示系统状态信息
QLabel* label_;

public:
// SysStatusDisplay 构造函数,初始化节点名为 "sys_status_display"
SysStatusDisplay() : Node("sys_status_display") {
// 初始化 QLabel 控件
label_ = new QLabel();

// 创建一个订阅者,订阅 "sys_status" 话题,消息队列大小为10
subscriber_ = this->create_subscription<SystemStatus>(
"sys_status", 10,
[&](const SystemStatus::SharedPtr msg) -> void {
// 每当接收到消息时,更新标签文本
label_->setText(get_qstr_from_msg(msg));
});

// 初始化时,先显示一个默认的空状态
label_->setText(get_qstr_from_msg(std::make_shared<SystemStatus>()));

// 显示 QLabel 控件
label_->show();
};

// 将接收到的 SystemStatus 消息转换为 QString 类型的文本
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_available << "\tMB\n" <<
"网络发送数据量:\t" << msg->net_sent << "\tMB\n" <<
"网络接收数据量:\t" << msg->net_recv << "\tMB\n" <<
"====================================";

// 将构造的字符串转换为 QString 类型返回
return QString::fromStdString(show_str.str());
};
};

// main 函数,程序入口
int main(int argc, char** argv) {

// 初始化 ROS2 客户端库
rclcpp::init(argc, argv);

// 初始化 Qt 应用程序
QApplication app(argc, argv);

// 创建 SysStatusDisplay 对象(ROS2节点和GUI控件)
auto node = std::make_shared<SysStatusDisplay>();

// 创建并启动一个新的线程,运行 ROS2 节点的 spin 方法
std::thread spin_thread([&]()->void{
// 运行 ROS2 事件循环,处理订阅和回调
rclcpp::spin(node);
});
// 分离线程,使其在后台继续执行
spin_thread.detach();

// 启动 Qt 应用程序的事件循环,开始界面显示并处理用户交互
app.exec(); // 阻塞,直到应用程序结束

return 0; // 程序正常退出
}

在 CMakeLists 中添加相应语句

1
2
3
4
5
6
7
8
9
# 添加可执行文件
add_executable(sys_status_display src/sys_status_display.cpp)
# 添加依赖库
target_link_libraries(sys_status_display Qt5::Widgets)
ament_target_dependencies(sys_status_display rclcpp status_interfaces)
# 拷贝节点到 install/lib
install(TARGETS hello_qt sys_status_display
DESTINATION lib/${PROJECT_NAME}
)

然后构建运行(提前运行 sys_status_pub 节点)

1
2
3
colcon build
source install/setup.bash
ros2 run status_display sys_status_display