4. 通信机制实战(上):话题通信——发布者与订阅者实现、自定义消息接口、QoS配置策略
好,我们进入通信机制实战环节。说实话,话题通信是 ROS2 里最核心、最常用的通信方式。你想想看,机器人身上那么多传感器、执行器,它们之间怎么高效地传递数据?靠的就是话题。这一章,我会带你亲手实现一个发布者和订阅者,然后聊聊自定义消息和 QoS 配置——这些都是我实际项目中反复踩坑后总结出来的经验。
4.1 发布者与订阅者的基本实现
先从一个最简单的例子开始。假设我们要发布一个字符串消息,订阅者收到后打印出来。嗯,这就像广播电台和收音机的关系——发布者只管发,订阅者只管收,双方不需要知道对方是谁。
4.1.1 编写发布者节点
我个人习惯用 C++ 写 ROS2 节点,因为性能更好。不过 Python 也完全没问题,看你的项目需求。下面是一个标准的发布者代码:
// publisher.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class SimplePublisher : public rclcpp::Node
{
public:
SimplePublisher() : Node("simple_publisher")
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
std::bind(&SimplePublisher::timer_callback, this));
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, ROS2!";
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<SimplePublisher>());
rclcpp::shutdown();
return 0;
}
4.1.2 编写订阅者节点
订阅者这边更简单。它只需要注册一个回调函数,当有消息到达时自动触发:
// subscriber.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class SimpleSubscriber : public rclcpp::Node
{
public:
SimpleSubscriber() : Node("simple_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&SimpleSubscriber::topic_callback, this, std::placeholders::_1));
}
private:
void topic_callback(const std_msgs::msg::String::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<SimpleSubscriber>());
rclcpp::shutdown();
return 0;
}
4.2 自定义消息接口
标准消息类型(比如 String、Int32)只能满足简单需求。实际项目中,你往往需要传递更复杂的数据结构。比如,我要发布一个包含位置和姿态的机器人状态消息,那就得自己定义接口。
4.2.1 创建自定义消息文件
在 ROS2 中,自定义消息放在 .msg 文件中。假设我们创建一个 RobotStatus.msg:
# RobotStatus.msg
int32 robot_id
string robot_name
float64 position_x
float64 position_y
float64 orientation_z
bool is_moving
这个文件放在你的功能包里的 msg 目录下。然后需要在 CMakeLists.txt 中添加:
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/RobotStatus.msg"
)
以及在 package.xml 中添加依赖:
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
geometry_msgs/Point 来表示位置。
4.2.2 在代码中使用自定义消息
编译成功后,你就可以像使用标准消息一样使用自定义消息了:
#include "your_package/msg/robot_status.hpp"
// 发布者
auto publisher = this->create_publisher<your_package::msg::RobotStatus>("robot_status", 10);
// 创建消息
auto msg = your_package::msg::RobotStatus();
msg.robot_id = 1;
msg.robot_name = "TurtleBot";
msg.position_x = 1.5;
嗯,这里要注意:消息字段的命名风格是蛇形命名法(snake_case),ROS2 会自动帮你转换成驼峰命名法(camelCase)的 C++ 代码。
4.3 QoS 配置策略
QoS(Quality of Service)是 ROS2 相比 ROS1 最大的改进之一。说白了,它让你能精细控制消息的可靠性、持久性等属性。我刚开始接触时也觉得复杂,但用熟了就会发现它真的很强大。
4.3.1 QoS 的核心参数
| 参数 | 说明 | 常用值 |
|---|---|---|
| Reliability | 可靠性策略 | RELIABLE(保证送达)/ BEST_EFFORT(尽力而为) |
| Durability | 持久性策略 | VOLATILE(不保留)/ TRANSIENT_LOCAL(保留最后一条) |
| Depth | 队列深度 | 通常设为 10 |
| Liveliness | 活性检测 | AUTOMATIC / MANUAL_BY_TOPIC |
4.3.2 实际配置示例
假设你有一个激光雷达,数据量很大但允许丢帧。这时候用 BEST_EFFORT 策略最合适:
auto qos = rclcpp::QoS(rclcpp::KeepLast(10));
qos.reliability(RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT);
qos.durability(RMW_QOS_POLICY_DURABILITY_VOLATILE);
auto subscription = this->create_subscription<sensor_msgs::msg::LaserScan>(
"scan", qos, callback);
反过来,如果是控制指令,必须保证送达,那就用 RELIABLE:
auto qos = rclcpp::QoS(rclcpp::KeepLast(10));
qos.reliability(RMW_QOS_POLICY_RELIABILITY_RELIABLE);
qos.durability(RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL);
4.4 本章知识体系
下面这张图展示了话题通信的核心流程和本章涉及的关键概念:
4.5 实战经验总结
最后,分享几个我在项目中积累的经验:
- 消息类型选择: 能用标准消息就别自己定义。比如位置信息用
geometry_msgs/Pose,图像用sensor_msgs/Image,这样能保证和其他包的兼容性。 - QoS 调试技巧: 如果发现消息收不到,先用
ros2 topic echo /topic_name看看话题上有没有数据。再用ros2 topic info /topic_name查看发布者和订阅者的 QoS 配置。 - 性能优化: 高频话题(比如 IMU 数据)建议用
BEST_EFFORT和较小的队列深度,避免内存暴涨。低频控制指令用RELIABLE保证可靠性。 - 命名规范: 自定义消息的包名、消息名、字段名都要有意义。我见过有人用
msg1.msg这种名字,三个月后自己都看不懂了。
.msg 文件里写清楚每个字段的注释。这样不仅方便自己,也方便团队协作。你想想看,半年后回头维护代码,注释就是最好的文档。