4、ROS2节点编程基础:节点概念、rclcpp库入门、编写第一个发布者节点、编写第一个订阅者节点、使用rclpy(Python版)
好,咱们进入正题。这一章是ROS2编程的起点,说白了就是学会怎么让机器人“说话”和“听话”。
我刚开始学ROS的时候,最困惑的就是“节点”到底是个啥玩意儿。后来在项目里调试一个机械臂,折腾了一整天才明白——节点其实就是一个个独立的小程序,各干各的活,通过话题互相传数据。嗯,理解了这一点,后面就顺了。
4.1 节点概念:ROS2里的“小工人”
ROS2的节点,你可以把它想象成一个工厂里的工人。每个工人只负责一件事:
- 有的工人专门负责拿传感器数据(比如激光雷达节点)
- 有的工人专门负责算路径(比如导航节点)
- 有的工人专门负责控制电机(比如底盘驱动节点)
这些工人之间怎么沟通?通过“话题”(Topic)。工人A把数据发布到话题上,工人B订阅这个话题就能拿到数据。这就是发布-订阅模式。
核心要点:一个ROS2系统就是由多个节点组成的分布式网络。每个节点都是独立的可执行文件,节点之间通过话题、服务、动作等机制通信。
我个人习惯把节点设计得尽量“小”——一个节点只做一件事,做好一件事。这样调试起来特别方便,哪个节点出问题,直接换掉就行,不影响其他部分。
4.2 rclcpp库入门:C++版本的ROS2客户端库
rclcpp,全称是ROS Client Library for C++。说白了,它就是ROS2官方给C++开发者准备的“工具箱”。
你想想看,如果没有这个库,你要自己处理节点创建、话题通信、参数管理这些底层细节,那得多痛苦?rclcpp把这些都封装好了,我们直接调用API就行。
先看一个最简单的rclcpp程序框架:
#include <rclcpp/rclcpp.hpp>
int main(int argc, char **argv)
{
// 初始化ROS2
rclcpp::init(argc, argv);
// 创建一个节点,名字叫"my_first_node"
auto node = std::make_shared<rclcpp::Node>("my_first_node");
// 打印一条日志
RCLCPP_INFO(node->get_logger(), "节点已启动!");
// 让节点保持运行
rclcpp::spin(node);
// 关闭ROS2
rclcpp::shutdown();
return 0;
}
这段代码里,rclcpp::init是必须的第一步,它负责和ROS2底层通信。然后创建节点,rclcpp::spin让节点一直活着,等待接收消息或执行回调。最后rclcpp::shutdown做清理工作。
小技巧:我在项目中经常用RCLCPP_INFO来打印调试信息。它比std::cout好使,因为会自动带上时间戳和节点名,排查问题一目了然。
4.3 编写第一个发布者节点
发布者节点,就是那个“说话”的节点。它定期往话题上发数据。
我记得第一次写发布者节点时,犯了个低级错误——忘记设置发布频率,结果数据像机关枪一样往外蹦,把订阅者节点直接搞崩了。后来学乖了,一定要用rclcpp::Rate来控制节奏。
来看一个完整的发布者节点示例,它每隔1秒发布一个“Hello ROS2”字符串:
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
// 创建节点
auto node = std::make_shared<rclcpp::Node>("talker");
// 创建发布者,话题名是"chatter",队列大小10
auto publisher = node->create_publisher<std_msgs::msg::String>("chatter", 10);
// 创建消息对象
std_msgs::msg::String message;
message.data = "Hello ROS2!";
// 设置循环频率为1Hz(每秒1次)
rclcpp::Rate rate(1);
while (rclcpp::ok())
{
// 发布消息
publisher->publish(message);
RCLCPP_INFO(node->get_logger(), "发布了: '%s'", message.data.c_str());
// 休眠,保持频率
rate.sleep();
}
rclcpp::shutdown();
return 0;
}
这里有几个关键点:
create_publisher的第一个参数是消息类型,第二个参数是队列大小。队列大小决定了如果订阅者处理不过来,最多缓存多少条消息。rclcpp::ok()检查ROS2是否还在正常运行,按Ctrl+C会返回false,从而退出循环。rate.sleep()控制发布频率,1Hz就是每秒一次。
避坑指南:我曾经在队列大小这里吃过亏。设成1的话,如果订阅者处理慢,消息就会丢。设成1000又太占内存。一般10-100是比较合理的范围,具体看你的消息大小和处理速度。
4.4 编写第一个订阅者节点
有说的,就得有听的。订阅者节点就是那个“听话”的节点。
订阅者的工作模式是“回调驱动”——它注册一个回调函数,一旦话题上有新消息,ROS2就会自动调用这个函数。你不用写循环,不用轮询,省心得很。
来看订阅者节点的代码:
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
// 回调函数:当收到消息时被调用
void topic_callback(const std_msgs::msg::String::SharedPtr msg)
{
RCLCPP_INFO(rclcpp::get_logger("listener"), "收到了: '%s'", msg->data.c_str());
}
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
// 创建节点
auto node = std::make_shared<rclcpp::Node>("listener");
// 创建订阅者,订阅"chatter"话题
auto subscription = node->create_subscription<std_msgs::msg::String>(
"chatter", 10, topic_callback);
// 进入事件循环,等待回调
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
看到没?订阅者比发布者还简单。核心就三步:
- 定义回调函数
- 创建订阅者,绑定话题和回调
- 调用
rclcpp::spin进入事件循环
你可能会问:为什么发布者用while循环,订阅者用spin?
因为发布者是主动的,它要控制什么时候发、发多少。订阅者是被动的,它只需要等着,有消息来了就处理。所以订阅者用spin最合适,它会一直阻塞,直到收到消息才执行回调。
4.5 使用rclpy(Python版)
如果你更喜欢Python,ROS2也提供了rclpy库。说实话,我平时做原型验证时更喜欢用Python,写起来快,调试也方便。但做正式产品时,我会用C++,性能更好。
先看Python版的发布者:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class TalkerNode(Node):
def __init__(self):
super().__init__('talker')
self.publisher_ = self.create_publisher(String, 'chatter', 10)
self.timer = self.create_timer(1.0, self.timer_callback) # 每秒触发一次
self.i = 0
def timer_callback(self):
msg = String()
msg.data = 'Hello ROS2: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('发布了: "%s"' % msg.data)
self.i += 1
def main(args=None):
rclpy.init(args=args)
node = TalkerNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Python版和C++版的核心逻辑一模一样,只是语法不同。注意Python版用了create_timer来替代rclcpp::Rate,这是rclpy的特色——用定时器来触发周期性任务。
再看Python版的订阅者:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class ListenerNode(Node):
def __init__(self):
super().__init__('listener')
self.subscription = self.create_subscription(
String,
'chatter',
self.listener_callback,
10)
self.subscription # 防止被垃圾回收
def listener_callback(self, msg):
self.get_logger().info('收到了: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
node = ListenerNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Python版的订阅者同样简洁。有个小细节:self.subscription这一行看起来多余,但如果不赋值给成员变量,Python的垃圾回收可能会把订阅者对象给回收掉,导致收不到消息。嗯,这个坑我踩过。
4.6 编译与运行
代码写好了,怎么跑起来?
对于C++版本,你需要用colcon build编译。在ROS2工作空间的src目录下创建包,把代码放进去,然后:
cd ~/ros2_ws
colcon build --packages-select 你的包名
source install/setup.bash
ros2 run 你的包名 talker
ros2 run 你的包名 listener
对于Python版本,不需要编译,直接运行:
ros2 run 你的包名 talker
ros2 run 你的包名 listener
打开两个终端,一个跑发布者,一个跑订阅者,你会看到订阅者终端里不断打印出收到的消息。这就是ROS2节点通信的基本流程。
调试小工具:用ros2 topic list可以查看当前所有活跃的话题。用ros2 topic echo /chatter可以直接在终端里监听话题数据,不用写代码就能验证发布者是否正常工作。
好了,这一章的内容就到这里。节点编程是ROS2的基石,后面的所有高级功能——服务、动作、参数、生命周期——都建立在这个基础之上。把发布者和订阅者练熟了,后面的路就好走了。