3. ROS2实战:编写第一个发布订阅节点、编译与运行、使用rqt工具可视化
好,咱们开始动手了。
前面聊了那么多中间件的理论,说实话,不写几行代码总觉得心里不踏实。这一章,我带你亲手搭建一个ROS2的发布订阅节点。你想想看,这其实就是自动驾驶系统里最基础的通信单元——一个节点发消息,另一个节点收消息。搞懂了它,后面那些复杂的传感器融合、规划控制,本质上都是这个模式的延伸。
3.1 准备工作:工作空间与包结构
我个人习惯,先把工作空间搭好。ROS2里叫workspace,说白了就是一个放代码的文件夹。
# 创建工作空间
mkdir -p ~/autoware_ws/src
cd ~/autoware_ws/src
# 创建功能包
ros2 pkg create --build-type ament_cmake --node-name talker_node beginner_tutorials
这里我用了ament_cmake,因为C++在自动驾驶里更常见。如果你偏爱Python,也可以用ament_python。嗯,这里要注意:包名不要用大写字母,ROS2的规范是小写加下划线。
创建完成后,你会看到这样的目录结构:
beginner_tutorials/
├── CMakeLists.txt
├── package.xml
├── src/
│ └── talker_node.cpp
└── include/
└── beginner_tutorials/
src/目录下再建一个nodes/文件夹,把不同功能的节点分开。项目大了你就知道,这样找代码快很多。
3.2 编写发布者节点(Talker)
打开src/talker_node.cpp,我们来写第一个发布者。说白了,就是每隔一秒发一条"Hello, 自动驾驶!"的消息。
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class TalkerNode : public rclcpp::Node
{
public:
TalkerNode() : Node("talker_node")
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic_chatter", 10);
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
std::bind(&TalkerNode::timer_callback, this));
RCLCPP_INFO(this->get_logger(), "发布者节点已启动");
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, 自动驾驶!当前时间: " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "发布: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
int count_ = 0;
};
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<TalkerNode>());
rclcpp::shutdown();
return 0;
}
这段代码里,我用了create_wall_timer来定时触发。为什么不用sleep?因为sleep会阻塞整个线程,而定时器是异步的,不影响节点处理其他回调。我在项目中遇到过,有人用sleep写节点,结果一跑起来,订阅端收不到消息,排查了半天才发现是定时器用错了。
3.3 编写订阅者节点(Listener)
有发就得有收。我们再写一个订阅者节点,放在同一个包里。
在src/目录下新建listener_node.cpp:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class ListenerNode : public rclcpp::Node
{
public:
ListenerNode() : Node("listener_node")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic_chatter", 10,
std::bind(&ListenerNode::topic_callback, this, std::placeholders::_1));
RCLCPP_INFO(this->get_logger(), "订阅者节点已启动,等待消息...");
}
private:
void topic_callback(const std_msgs::msg::String::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "收到: '%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<ListenerNode>());
rclcpp::shutdown();
return 0;
}
你看,订阅者的结构和发布者很像。区别在于,它注册了一个回调函数topic_callback,一旦有消息到达,这个函数就会被调用。说白了,就是"你发我就收,你不发我等着"。
3.4 修改CMakeLists.txt
代码写完了,得告诉编译器怎么编译。打开CMakeLists.txt,在文件末尾添加:
add_executable(talker_node src/talker_node.cpp)
ament_target_dependencies(talker_node rclcpp std_msgs)
add_executable(listener_node src/listener_node.cpp)
ament_target_dependencies(listener_node rclcpp std_msgs)
install(TARGETS
talker_node
listener_node
DESTINATION lib/${PROJECT_NAME}
)
package.xml里添加rclcpp和std_msgs的依赖。结果编译报错,提示找不到头文件。所以记得检查package.xml,确保有这两行:
<depend>rclcpp</depend>
<depend>std_msgs</depend>
3.5 编译与运行
回到工作空间根目录,开始编译:
cd ~/autoware_ws
colcon build --packages-select beginner_tutorials
编译成功后,记得source一下环境:
source install/setup.bash
然后,开两个终端,分别运行发布者和订阅者:
终端1(发布者):
ros2 run beginner_tutorials talker_node
终端2(订阅者):
ros2 run beginner_tutorials listener_node
你会看到,发布者每秒打印一条发送的消息,订阅者同步打印收到的消息。嗯,这就是最基础的发布订阅通信。
3.6 使用rqt工具可视化
光看终端日志不过瘾,咱们用rqt来可视化。rqt是ROS2的图形化工具集,说白了就是给你一个GUI,让你看到节点、话题、服务这些内部结构。
先启动rqt:
rqt
在rqt的菜单栏里,选择 Plugins > Topics > Topic Monitor。你会看到topic_chatter这个话题,点击它,就能看到消息的实时内容。
我个人最常用的是rqt_graph,它能画出节点之间的通信关系图。在插件菜单里找到它,你会看到:
这张图很直观吧?/talker_node把消息推到/topic_chatter上,/listener_node从上面取。箭头方向就是数据流的方向。
3.7 避坑指南
最后,分享几个我踩过的坑:
- 话题名称拼写错误:发布者和订阅者的话题名必须完全一致,大小写敏感。我曾经因为把
chatter拼成chater,排查了半小时。 - 忘记source环境:每次新开终端,都要
source install/setup.bash。我建议把它加到~/.bashrc里,省得每次都手动敲。 - QoS设置不匹配:ROS2的QoS(服务质量)如果发布者和订阅者设置不一致,可能导致收不到消息。初学者先用默认的
10,后面再深入调优。 - 节点名冲突:同一个网络中,节点名不能重复。如果你同时跑两个
talker_node,ROS2会报错。可以用ros2 run ... --ros-args -r __node:=new_name来重命名。
好了,这一章的内容就到这里。代码不多,但每一步都是自动驾驶中间件开发的基石。你动手跑一遍,比看十遍文档都管用。