3、发布者节点实现:编写Python发布者节点、设置发布频率、发布消息数据

好,咱们接着聊。上一节我们把话题通信的底层逻辑理清了,现在该动手了。

这一节,我带你亲手写一个发布者节点。说白了,就是让机器人里的某个程序,能定时往外喊话:「我这里有地图数据,谁要谁拿走!」

我个人习惯,学ROS2先从Python入手。为什么?因为快。你写几行代码就能跑起来,看到效果,信心就来了。C++当然更高效,但调试起来麻烦不少。咱们先跑通,再优化。

3.1 创建功能包与节点文件

在开始写代码之前,你得先有个「家」——也就是功能包。我建议你直接在终端里敲命令,别用IDE新建,那样容易漏掉依赖配置。

ros2 pkg create map_publisher_pkg --build-type ament_python --dependencies rclpy std_msgs nav_msgs

这条命令干了三件事:

  • 创建了一个叫 map_publisher_pkg 的功能包
  • 指定了Python作为开发语言
  • 自动添加了三个依赖:rclpy(ROS2客户端库)、std_msgs(标准消息类型)、nav_msgs(导航相关消息,地图数据就在这里面)

嗯,这里要注意:nav_msgs 里的 OccupancyGrid 消息类型,就是咱们传输地图数据的核心。它本质上是一个二维数组,每个格子存一个值:0表示空闲,100表示障碍,-1表示未知。

3.2 编写发布者节点代码

接下来,在 map_publisher_pkg/map_publisher_pkg/ 目录下新建一个文件,叫 map_publisher.py。我直接贴代码,然后一行行给你拆解。

import rclpy
from rclpy.node import Node
from nav_msgs.msg import OccupancyGrid
from std_msgs.msg import Header
import numpy as np

class MapPublisher(Node):

    def __init__(self):
        super().__init__('map_publisher')
        self.publisher_ = self.create_publisher(OccupancyGrid, 'map_topic', 10)
        timer_period = 1.0  # 每秒发布一次
        self.timer = self.create_timer(timer_period, self.timer_callback)
        self.get_logger().info('地图发布节点已启动!')

    def timer_callback(self):
        msg = OccupancyGrid()
        msg.header = Header()
        msg.header.stamp = self.get_clock().now().to_msg()
        msg.header.frame_id = 'map'

        # 设置地图元信息
        msg.info.resolution = 0.05  # 每个格子0.05米
        msg.info.width = 100
        msg.info.height = 100
        msg.info.origin.position.x = -2.5
        msg.info.origin.position.y = -2.5
        msg.info.origin.position.z = 0.0
        msg.info.origin.orientation.w = 1.0

        # 生成模拟地图数据
        map_data = np.zeros((100, 100), dtype=np.int8)
        # 在中间画一个障碍物方块
        map_data[40:60, 40:60] = 100
        # 随机加一些噪声点
        map_data[20:30, 70:80] = 50
        msg.data = map_data.flatten().tolist()

        self.publisher_.publish(msg)
        self.get_logger().info(f'发布了地图数据,尺寸:{msg.info.width}x{msg.info.height}')

def main(args=None):
    rclpy.init(args=args)
    map_publisher = MapPublisher()
    rclpy.spin(map_publisher)
    map_publisher.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

这段代码,我拆成几个关键点来讲。

3.3 核心机制拆解

第一,创建发布者。

self.create_publisher(OccupancyGrid, 'map_topic', 10) 这行,指定了消息类型是 OccupancyGrid,话题名叫 map_topic,队列深度是10。队列深度什么意思?就是如果订阅者处理不过来,最多缓存10条消息,再新的就丢掉。我在项目中遇到过,如果地图数据特别大,队列深度设太大反而会撑爆内存。10是个比较稳妥的值。

第二,定时器驱动。

self.create_timer(timer_period, self.timer_callback) 这行,设置了一个1秒的定时器。每过1秒,ROS2就会自动调用 timer_callback 函数。这就是发布频率的控制方式。你想10Hz?那就设0.1秒。想0.5Hz?设2秒。很简单。

第三,消息填充。

地图消息的核心是 msg.info 和 msg.data。info 告诉接收方:地图有多大、每个格子代表多少米、原点在哪。data 就是实实在在的格子数据。我这里用numpy生成了一个100x100的模拟地图,中间画了个障碍物方块。实际项目中,你会从SLAM算法或者加载文件里拿数据。

重要提醒: msg.data 必须是一维列表。如果你用二维数组,记得用 flatten() 压平。我曾经有个同事,忘了这步,调试了一下午才发现数据发出去全是乱的。

3.4 设置发布频率的讲究

发布频率不是随便设的。我整理了一张表,方便你对照选择:

应用场景 推荐频率 说明
静态地图加载 1 Hz 地图不变,发布一次就够了,但为了容错可以低频重复
实时SLAM建图 5-10 Hz 机器人移动时,地图需要快速更新
高精度定位 20-50 Hz 配合激光雷达或视觉,需要高频地图匹配
大尺寸地图传输 0.5-2 Hz 地图太大,频率高了网络带宽扛不住

你想想看,如果你发布一个1000x1000的地图,每个格子占1字节,那就是1MB的数据。10Hz发布,每秒就是10MB流量。WiFi扛得住吗?不一定。所以频率和地图尺寸要权衡。

3.5 运行与验证

代码写好了,怎么跑?两步走:

  1. 编译功能包:在工作空间根目录执行 colcon build --packages-select map_publisher_pkg
  2. 运行节点:ros2 run map_publisher_pkg map_publisher

你会看到终端每隔1秒打印一条日志:「发布了地图数据,尺寸:100x100」。这时候再开一个终端,用 ros2 topic echo /map_topic 就能看到原始数据流。

小技巧: 用 ros2 topic hz /map_topic 可以查看实际发布频率。如果发现频率低于你设定的值,说明你的回调函数执行时间太长了。这时候要么优化代码,要么降低频率。

3.6 避坑指南

我曾经在项目里犯过一个低级错误:忘了设置 msg.header.stamp 的时间戳。结果订阅端拿到的地图数据,时间全是0,导致整个定位系统时序错乱。所以记住:时间戳一定要用 self.get_clock().now().to_msg() 来生成,别自己手动填。

还有一个坑:frame_id 要跟你的TF树对得上。如果你写死了 'map',但实际TF树里叫 'global_map',那订阅端做坐标变换时会直接报错。我建议你把这个参数化,别写死。

3.7 本章知识体系

为了让你更直观地理解发布者节点的整体结构,我画了一张流程图:

发布者节点核心流程 MapPublisher 节点 初始化:创建发布者、定时器 create_publisher + create_timer 定时器触发 timer_callback() 频率由 timer_period 控制 构建 OccupancyGrid 消息 填充 header、info(分辨率/尺寸/原点)、data data 必须是一维列表 publish() 到 /map_topic

这张图把整个流程串起来了:节点启动 → 初始化发布者和定时器 → 定时触发回调 → 构建消息 → 发布出去。每一步都环环相扣,缺一不可。

好,发布者节点就讲到这里。代码不多,但背后的机制值得你多琢磨。下一节我们讲订阅者怎么接收这些地图数据,到时候你会看到,接收端其实更简单,但坑也不少。


可可精品资料库