ROS 快速入门基础使用教程

什么是发布和订阅

一、 核心问题:到底什么是“发布/订阅”?

避障机器人例子:一个节点发警告,一个节点收警告。这就是“发布/订阅”。

为了让你彻底理解,我们来打个比方:一个非常现代化的“编辑部”

1. 生动的比喻:现代化的“编辑部”

想象一个传统的报社,记者写完稿子,必须直接递给特定的编辑。如果换了编辑,记者就得重新认识。这就像古老的软件设计,每个部分都紧紧耦合在一起,一换人就乱套。

而ROS的“发布/订阅”模式,就像是一个现代化的、高效的“数字编辑部”

  • 发布者 (Publisher) = 前线记者:比如我们的“传感器记者”,他的任务就是源源不断地发回“前方有障碍物!”的快讯(数据)。他根本不知道谁会看这条快讯,他只管把稿子扔进一个叫“紧急快讯”的篮子里 。
  • 订阅者 (Subscriber) = 后端编辑:比如我们的“控制编辑”,他只关心“紧急快讯”。他不需要知道是哪个记者发的,只要看到这个篮子里有新稿子,就拿过来处理,然后发出“刹车!”的指令 。
  • 话题 (Topic) = 带有标签的篮子:这个篮子就是连接记者和编辑的唯一桥梁。篮子上有个显眼的标签,比如“/obstacle_warning”。所有对这个话题感兴趣的人,都盯着这个篮子看 。
  • 消息 (Message) = 稿件的标准格式:为了让所有人都能读懂,稿子有固定的格式。比如“障碍物警告”的稿子,格式就是“有/无 (True/False)”。这就是ROS里定义好的消息类型(如 Bool)。

你看,在这个编辑部里,记者和编辑完全解耦了。记者不用认识编辑,编辑也不用管记者是谁。他们只需要知道“篮子(话题)”和“稿子格式(消息)”就行了。这种灵活、高效、可扩展的模式,就是ROS通信的灵魂。

2. 为什么要这么设计?从ROS的起源故事说起

这么巧妙的设计,不是凭空想出来的,而是被“逼”出来的。让我们把时间拨回到2007年的斯坦福大学 。

一群天才研究生想造一个超级厉害的个人机器人(PR1),能帮你做家务、陪你聊天那种。但他们很快发现一个巨大的难题:机器人系统太复杂了!一个人要从零写出控制电机、读取激光雷达、做路径规划的所有代码,根本不可能 。

于是他们灵机一动:既然自己造不出所有轮子,那为什么不创造一个平台,让大家把自己造的“轮子”都贡献出来,互相分享呢?

他们希望,世界上任何一个实验室开发出的“牛逼导航算法”,能直接用在另一个实验室的机器人上,只需简单配置一下。这就是ROS诞生的初衷:“提高机器人软件复用率,不要重复造轮子”

要实现这个理想,就必须制定一套标准化的“接口”。这个接口就是“发布/订阅”机制。它把复杂的机器人功能,拆解成一个一个独立的“节点”。大家只需要遵循标准(用什么话题、传什么消息),就可以像玩乐高积木一样,把来自全世界的优秀代码拼成一个强大的机器人 。

二、 ROS到底是个什么东西?一套怎样的“框架”?

你问ROS是不是像Windows一样的操作系统,这是一个非常好的问题。答案是:ROS不是我们传统意义上的操作系统,而是一个运行在操作系统之上的“中间件”或“软件框架”

1. 形象理解:ROS是“机器人的神经系统 + 万能工具箱”
  • Windows/Linux 是你的“身体”:它们管理电脑的硬件(CPU、内存),这是基础。
  • ROS 是你的“神经系统”和“工具箱”
    • 神经系统:它负责把“眼睛”(摄像头节点)、“耳朵”(激光雷达节点)、“小脑”(运动控制节点)全部连接起来,让信息(神经冲动)能高速、有序地传递 。
    • 万能工具箱:它提供了一整套开发机器人所需的工具。比如 rqt_graph(让你看到神经连接图)、rviz(让你“看到”机器人眼中的世界)、gazebo(提供一个虚拟的训练场,让机器人在里面模拟跑动)等等 。

所以,所谓“框架”,就是一套为你搭好的、有规矩的骨架。你不需要关心“如何建立两个程序之间的网络连接”、“如何处理数据丢包”、“如何让不同语言写的程序对话”这些复杂问题。ROS这个框架都帮你搞定了,你只需要专心把你的“节点”(比如避障逻辑)写好,然后插到这个框架里就行 。

三、 技术实现:ROS是如何做到“发布/订阅”的?(以ROS 1为例)

理解了思想,我们再看看它到底是怎么实现的。虽然现在主流是ROS 2,但ROS 1的“媒婆”模型最简单直观,能帮你最快建立概念。

订阅者节点 (编辑) ROS Master (媒婆) 发布者节点 (记者) 订阅者节点 (编辑) ROS Master (媒婆) 发布者节点 (记者) 第一步:相亲登记 媒婆心里一乐: 这两人有戏! 第二步:私下交换联系方式 第三步:直接谈恋爱(传输数据) loop [源源不断地传输] 1. 我来了!我叫/talker, 我要发布“/chatter”话题! 2. 我来了!我叫/listener, 我要订阅“/chatter”话题! 3. 姑娘(订阅者), 有个叫/talker的小伙(发布者)在发/chatter, 他的地址是XXX, 快去找他! 4. 你好/talker, 我对你的/chatter感兴趣, 咱们用TCP连接吧? 5. 好的/listener, 这是我的数据, 你准备好收了吗? 6. 发送消息 (Hello World!)

这张图就是ROS 1通信的核心流程,我们可以把它总结为三个清晰的步骤 :

  1. 第一步:登记注册 (步骤1 & 2)
    每个节点启动后,第一件事就是去找一个叫 ROS Master 的特殊节点(你可以把它想象成一个勤劳的“媒婆”)。发布者告诉媒婆:“我要发布话题A!”;订阅者也告诉媒婆:“我对话题A感兴趣!”。

  2. 第二步:牵线搭桥 (步骤3)
    媒婆在自己的小本本上一查,发现发布者和订阅者对上了眼。于是,她偷偷把发布者的“家庭住址”(IP地址和端口号)告诉了订阅者 。

  3. 第三步:点对点通信 (步骤4-6)
    最关键的一步来了:媒婆完成任务后就“功成身退”了!真正的数据传输,是订阅者拿着地址,直接去找发布者建立连接(通常是TCP连接),然后发布者就把数据源源不断地发给订阅者 。

这个设计极其精妙:媒婆(Master)只负责建立连接,不负责传数据。这就避免了中心节点成为整个系统的瓶颈。哪怕媒婆挂了,已经建立好的连接依然可以正常工作!

四、 总结与展望

所以,让我们来回顾一下:

  • 发布/订阅 就像编辑部,通过一个“话题篮子”将“记者”和“编辑”解耦,让他们各自专注自己的工作。
  • 这么设计的原因,是为了实现 “软件复用” 的伟大理想,让全世界的开发者能像玩乐高一样拼装机器人 。
  • ROS本身 不是操作系统,而是一个运行在操作系统上的 “机器人开发框架”,它提供了通信的“神经系统”和大量的“万能工具” 。
  • 技术实现上,依靠一个临时的“媒婆”(Master)牵线,之后让节点之间直接“谈恋爱”(点对点通信),保证了系统的高效和健壮 。

现在, 从思想到技术,对ROS有了一个全方位的认识。ROS 2在ROS 1的基础上,把这个“媒婆”去掉了,换成了更先进的“自动发现”机制(基于DDS技术),让系统更加健壮、实时性更好,能适应更多工业级场景 。但万变不离其宗,“发布/订阅”这个核心思想,以及“节点、话题、消息”这些核心概念,是你在ROS世界里永远最趁手的兵器。

ROS 不仅仅是“发消息”,而是一个机器人开发框架,它的核心思想是节点(Node)之间的通信。
ROS 核心概念(快速理解)

  1. 核心三要素
  • 节点(Node):独立运行的进程(程序),例如:传感器驱动、运动控制、算法模块

  • 话题(Topic):异步通信方式,一方发布(publish),多方订阅(subscribe) ← 你说的“发消息”主要在这里

  • 服务(Service):同步请求-响应模式,像函数调用

  1. ROS 2是否被公司大规模使用?
    是的,而且是正在大规模采用的路上。 现在的格局很清晰:科研和遗留系统用ROS 1,而所有新的、准备落地的产品都在用ROS 2

    • 行业巨头站台:全球最大的工业机器人公司FANUC(发那科)已经正式支持ROS 2,用于结合NVIDIA的AI技术打造下一代智能制造系统 。
    • 招聘市场的硬指标:在2026年的机器人招聘技能雷达图中,ROS 2和C++被列为最核心的、必须掌握的技能
    • 实际落地案例:在2025年底的ROS-Industrial大会上,来自西门子、Universal Robots等公司的工程师分享了大量ROS 2在生产环境中的部署经验,包括机器人抛光、焊接、物流等 。例如,美国最大的光学元件制造商Optimax,已经使用基于ROS 2的方案来制造用于火星探测器上的镜头,解决了旧系统的延迟和不平滑问题 。
  2. 公司主要用C++还是Python?
    答案是:两个都用,但各有分工。

    • C++是“心脏”:用于底层控制、驱动、实时性要求高的模块。比如机器人关节的伺服控制、运动规划的核心算法。因为C++性能高、延迟低 。
    • Python是“大脑”:用于高层逻辑、算法原型验证、AI集成、脚本编写。比如调用摄像头识别物体、决定机器人下一步该干什么、与用户交互 。
    • 你的策略:用Python快速上手和理解流程,然后根据需要,把性能瓶颈部分用C++重写。
  3. ROS的典型应用场景和开发任务是什么?

    • 典型场景:自动驾驶、工业机械臂(如焊接、抛光、分拣)、仓储移动机器人(AMR)、服务机器人(如酒店送餐)、无人机巡检等 。
    • 核心开发任务“让机器人‘看见’环境,并做出‘反应’”。这背后就是感知 -> 规划 -> 控制的闭环。而ROS 2的核心作用,就是把实现这三个环节的不同程序(节点)连接起来,让它们高效通信。

实战任务:打造一台能“思考”的自动避障机器人

为了让任务贴近真实,我们不再用那个只会“Hello World”的发消息例子。我们将打造一个自主避障机器人。这是几乎所有移动机器人的基础功能。

任务目标

编写两个程序(ROS 2节点):

  1. 传感器节点:模拟一个激光雷达,不断发布“前方有障碍物!”的警告。
  2. 控制节点:接收这个警告,并发出“刹车”或“转弯”的命令。
核心要素图解

在你写代码前,先记住这张图,这是ROS 2的“灵魂”:

“发布(Publish)”

“订阅(Subscribe)”

“调用(Call)”

“响应(Response)”

“发布(Publish)”

“订阅”

传感器节点
Sensor Node

话题
Topic
“/obstacle_warning”

控制节点
Control Node

服务
Service
“/emergency_stop”

话题
“/cmd_vel”

机器人硬件节点
Robot Hardware

  • 节点:每个独立运行的程序(如我们的sensor_nodecontrol_node)。
  • 话题异步通信,数据流。一个节点发消息,不用等回应,多个节点可以同时收。就像电台广播。这是最最常用的通信方式。
  • 服务同步通信,请求-响应。一个节点发送请求,必须等到回应才能继续。就像打电话,一问一答。
动手实现(Python)

1. 创建工作空间和包

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
colcon build
source install/setup.bash
cd src
ros2 pkg create --build-type ament_python my_robot_brain --dependencies rclpy std_msgs example_interfaces

2. 编写传感器节点 (sensor_node.py)
这个节点负责“感知”。它会定期(每秒2次)随机发布一个障碍物警告。

# 文件位置: ~/ros2_ws/src/my_robot_brain/my_robot_brain/sensor_node.py
#!/usr/bin/env python3
import rclpy  # ROS 2 Python客户端库
from rclpy.node import Node  # 所有节点都必须继承Node类
from std_msgs.msg import Bool  # 使用ROS定义好的布尔值消息类型

class SensorNode(Node):
    def __init__(self):
        super().__init__('sensor_node')  # 节点名字
        # 1. 创建一个发布者,发布到 'obstacle_warning' 话题,消息类型是Bool
        self.publisher_ = self.create_publisher(Bool, 'obstacle_warning', 10)
        # 2. 创建一个定时器,每秒触发2次,每次触发调用 `timer_callback` 函数
        self.timer = self.create_timer(0.5, self.timer_callback)
        self.get_logger().info("传感器节点已启动,正在探测障碍物...")

    def timer_callback(self):
        msg = Bool()
        # 模拟探测:随机产生一个障碍物警告 (True 表示有障碍)
        import random
        msg.data = random.choice([True, False])
        # 3. 发布消息!
        self.publisher_.publish(msg)
        self.get_logger().info(f'发布障碍物警告: {msg.data}')

def main(args=None):
    rclpy.init(args=args)
    node = SensorNode()
    rclpy.spin(node)  # 保持节点运行,直到被停止
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

3. 编写控制节点 (control_node.py)
这个节点是“大脑”。它订阅警告,收到True就紧急刹车,并调用一个服务来记录日志。

# 文件位置: ~/ros2_ws/src/my_robot_brain/my_robot_brain/control_node.py
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Bool
from geometry_msgs.msg import Twist  # 机器人速度指令的标准消息类型
from example_interfaces.srv import SetBool  # 一个标准的服务类型:设置True/False

class ControlNode(Node):
    def __init__(self):
        super().__init__('control_node')
        # 1. 创建一个订阅者,订阅 'obstacle_warning' 话题,收到消息时调用 'warning_callback'
        self.subscriber = self.create_subscription(Bool, 'obstacle_warning', self.warning_callback, 10)
        
        # 2. 创建一个发布者,用来向机器人硬件发送速度指令 (话题通常是 '/cmd_vel')
        self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10)
        
        # 3. 创建一个客户端,用来调用 '/log_safety_event' 这个服务
        self.cli = self.create_client(SetBool, '/log_safety_event')
        # 等待服务启动
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().warn('等待记录服务...')
        
        self.get_logger().info("控制节点已启动,准备避障...")

    def warning_callback(self, msg):
        # 当收到障碍物警告时,这个函数被自动调用
        twist = Twist()  # 创建一个速度指令
        
        if msg.data:  # 如果有障碍物
            self.get_logger().warn('检测到障碍物!紧急刹车!')
            # 刹车:速度和转速都设为0
            twist.linear.x = 0.0
            twist.angular.z = 0.0
            
            # 调用服务,记录这次安全事件
            request = SetBool.Request()
            request.data = True
            request.message = "障碍物触发刹车"
            future = self.cli.call_async(request)  # 异步调用,不阻塞主循环
            
        else:  # 如果没有障碍物
            self.get_logger().info('道路通畅,前进!')
            # 前进:速度0.2米/秒
            twist.linear.x = 0.2
            twist.angular.z = 0.0
        
        # 发布速度指令
        self.cmd_pub.publish(twist)

def main(args=None):
    rclpy.init(args=args)
    node = ControlNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

注意:为了让代码更清晰,我们没有真正实现一个/log_safety_event的服务节点。在实际项目中,会有一个专门的节点来记录日志或监控状态。

运行和观察
  1. 编译工作空间
    cd ~/ros2_ws
    colcon build
    source install/setup.bash
    
  2. 打开三个终端,分别运行
    • 终端1ros2 run my_robot_brain sensor_node
    • 终端2ros2 run my_robot_brain control_node
    • 终端3ros2 topic echo /cmd_vel (这个命令可以让你实时看到控制节点发出的速度指令)

你会看到控制节点根据传感器节点的警告,在“前进”和“刹车”之间切换。你还可以用以下命令“透视”整个系统:

  • ros2 node list:查看当前运行的节点。
  • ros2 topic list:查看当前所有的通信话题。
  • ros2 topic info /obstacle_warning:查看这个话题的发布者和订阅者信息。
  • rqt_graph:这个命令会打开一个图形化界面,非常直观地显示所有节点和它们之间的通信连接图。快去试试!

总结与下一步

通过这个实战,你已经亲手实践了ROS 2最核心的要素:

  • 节点:你写了两个独立的程序(sensor_nodecontrol_node)。
  • 话题obstacle_warning/cmd_vel,实现了传感器和控制器、控制器和硬件之间的广播式通信
  • 服务/log_safety_event,实现了一问一答的远程调用

就是机器人公司里使用最多的部分:将复杂的机器人功能(感知、规划、控制、硬件驱动)拆分成多个节点,然后用话题服务将它们组合成一个智能系统。你现在已经具备了看懂和编写大多数ROS 2基础功能的能力。

下一步,可以尝试将sensor_node改为读取一个真实的激光雷达数据(比如/scan话题),并实现一个更智能的避障算法(比如哪边空就往哪边转)。这就进入真正的机器人开发了!

用C++实现一个智能避障节点

非常好!从Python转到C++是走向专业ROS 2开发的必经之路。我会带你从零开始,用C++实现一个智能避障节点,并解释每一步在做什么。

📋 准备工作:理解我们要做什么

我们将创建一个节点,它:

  1. 订阅激光雷达数据(/scan话题)
  2. 分析前方左右两侧的障碍物距离
  3. 决策:哪边空就往哪边转,实现智能避障
  4. 发布速度指令(/cmd_vel话题)给机器人

🚀 第一步:创建C++包

1.1 创建工作空间(如果还没有)

# 创建并进入工作空间
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws

1.2 创建C++功能包

cd src
ros2 pkg create --build-type ament_cmake obstacle_avoider \
  --dependencies rclcpp sensor_msgs geometry_msgs

这条命令做了什么?

  • --build-type ament_cmake:指定这是一个C++包
  • obstacle_avoider:包名
  • --dependencies:自动添加依赖项
    • rclcpp:ROS 2的C++客户端库
    • sensor_msgs:包含激光雷达消息类型(LaserScan)
    • geometry_msgs:包含速度指令消息类型(Twist)

1.3 查看生成的包结构

cd obstacle_avoider
tree

你会看到:

obstacle_avoider/
├── CMakeLists.txt  # C++编译配置文件
├── package.xml     # 包信息文件
├── include/        # 头文件目录
│   └── obstacle_avoider/
└── src/            # 源代码目录

📝 第二步:编写智能避障节点

2.1 创建源代码文件

touch src/obstacle_avoider_node.cpp

2.2 写入完整代码

// 文件名: ~/ros2_ws/src/obstacle_avoider/src/obstacle_avoider_node.cpp

#include "rclcpp/rclcpp.hpp"                    // ROS 2核心库
#include "sensor_msgs/msg/laser_scan.hpp"       // 激光雷达消息
#include "geometry_msgs/msg/twist.hpp"          // 速度指令消息
#include <vector>                                // 用vector存激光数据
#include <string>                                // 字符串处理

using namespace std::placeholders;  // 用于 _1, _2 等占位符

/**
 * 智能避障节点类
 * 继承自 rclcpp::Node,这是所有ROS 2节点的基类 
 */
class ObstacleAvoiderNode : public rclcpp::Node
{
public:
    /**
     * 构造函数:在创建节点时自动调用
     */
    ObstacleAvoiderNode() : Node("obstacle_avoider_node")
    {
        // 1. 创建订阅者:订阅激光雷达话题
        //    sensor_msgs::msg::LaserScan 是激光雷达的标准消息类型 
        laser_sub_ = this->create_subscription<sensor_msgs::msg::LaserScan>(
            "scan",                              // 话题名(实际使用时可能需要重映射)
            10,                                  // 队列大小
            std::bind(&ObstacleAvoiderNode::laser_callback, this, _1)  // 回调函数
        );
        
        // 2. 创建发布者:发布速度指令
        //    geometry_msgs::msg::Twist 是速度指令的标准消息类型 
        cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>("cmd_vel", 10);
        
        // 3. 打印启动信息
        RCLCPP_INFO(this->get_logger(), "智能避障节点已启动!");
        RCLCPP_INFO(this->get_logger(), "正在等待激光雷达数据...");
    }

private:
    /**
     * 激光雷达数据回调函数
     * 每当收到新的激光数据,这个函数就会被自动调用 
     * 
     * @param msg 激光雷达消息的共享指针
     */
    void laser_callback(const sensor_msgs::msg::LaserScan::SharedPtr msg)
    {
        // --- 步骤1:分析激光数据 ---
        
        // 获取激光雷达的扫描范围信息
        float angle_min = msg->angle_min;    // 起始角度(弧度)
        float angle_max = msg->angle_max;    // 结束角度(弧度)
        float angle_increment = msg->angle_increment;  // 角度增量
        
        // 计算总共有多少个激光点
        int num_readings = msg->ranges.size();
        
        // 打印调试信息(每10次回调打印一次,避免刷屏)
        static int callback_count = 0;
        if (callback_count++ % 10 == 0) {
            RCLCPP_DEBUG(this->get_logger(), "收到激光数据:%d个点,角度范围:[%.2f, %.2f]", 
                        num_readings, angle_min, angle_max);
        }
        
        // --- 步骤2:实现"哪边空往哪边转"的智能逻辑 ---
        
        // 将扫描范围分成左、中、右三个区域(假设激光扫描是从右到左)
        // 通常激光雷达的0度是正前方 
        int right_start = 0;
        int right_end = num_readings / 3;
        int center_start = num_readings / 3;
        int center_end = 2 * num_readings / 3;
        int left_start = 2 * num_readings / 3;
        int left_end = num_readings - 1;
        
        // 计算各个区域的最小距离(避开无效值inf和nan)
        float min_right = 100.0f;   // 初始化一个很大的值
        float min_center = 100.0f;
        float min_left = 100.0f;
        
        // 遍历右侧区域
        for (int i = right_start; i < right_end && i < num_readings; i++) {
            if (msg->ranges[i] > msg->range_min && msg->ranges[i] < msg->range_max) {
                if (msg->ranges[i] < min_right) {
                    min_right = msg->ranges[i];
                }
            }
        }
        
        // 遍历中央区域
        for (int i = center_start; i < center_end && i < num_readings; i++) {
            if (msg->ranges[i] > msg->range_min && msg->ranges[i] < msg->range_max) {
                if (msg->ranges[i] < min_center) {
                    min_center = msg->ranges[i];
                }
            }
        }
        
        // 遍历左侧区域
        for (int i = left_start; i < left_end && i < num_readings; i++) {
            if (msg->ranges[i] > msg->range_min && msg->ranges[i] < msg->range_max) {
                if (msg->ranges[i] < min_left) {
                    min_left = msg->ranges[i];
                }
            }
        }
        
        // --- 步骤3:决策与速度计算 ---
        
        auto cmd_msg = geometry_msgs::msg::Twist();  // 创建速度指令消息
        
        // 定义安全距离(米)
        const float SAFE_DISTANCE = 0.5f;
        
        // 智能决策逻辑 
        if (min_center > SAFE_DISTANCE) {
            // 前方安全,直行
            cmd_msg.linear.x = 0.3;      // 前进速度 0.3 m/s
            cmd_msg.angular.z = 0.0;      // 不转弯
            RCLCPP_INFO(this->get_logger(), "直行 - 前方:%.2fm", min_center);
        }
        else {
            // 前方有障碍,需要转弯
            // 比较左右哪边更空旷
            if (min_left > min_right && min_left > 0.2f) {
                // 左边更空旷,向左转
                cmd_msg.linear.x = 0.1;      // 缓慢前进
                cmd_msg.angular.z = 0.5;      // 左转(正值通常是逆时针/左转)
                RCLCPP_INFO(this->get_logger(), "左转 - 左:%.2fm 右:%.2fm", min_left, min_right);
            }
            else if (min_right > 0.2f) {
                // 右边更空旷,向右转
                cmd_msg.linear.x = 0.1;
                cmd_msg.angular.z = -0.5;     // 右转(负值是顺时针/右转)
                RCLCPP_INFO(this->get_logger(), "右转 - 左:%.2fm 右:%.2fm", min_left, min_right);
            }
            else {
                // 左右都被堵死,原地旋转找路
                cmd_msg.linear.x = 0.0;
                cmd_msg.angular.z = 0.8;      // 快速旋转
                RCLCPP_WARN(this->get_logger(), "被困住!原地旋转寻找出路");
            }
        }
        
        // --- 步骤4:发布速度指令 ---
        cmd_pub_->publish(cmd_msg);
    }
    
    // 成员变量声明
    rclcpp::Subscription<sensor_msgs::msg::LaserScan>::SharedPtr laser_sub_;  // 订阅者
    rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;        // 发布者
};

/**
 * 主函数:程序的入口
 */
int main(int argc, char * argv[])
{
    // 1. 初始化ROS 2 
    rclcpp::init(argc, argv);
    
    // 2. 创建节点实例(使用智能指针)
    auto node = std::make_shared<ObstacleAvoiderNode>();
    
    // 3. 保持节点运行,处理回调 
    //    spin() 会让节点一直运行,直到被 Ctrl+C 中断
    rclcpp::spin(node);
    
    // 4. 清理并关闭
    rclcpp::shutdown();
    return 0;
}

⚙️ 第三步:配置CMakeLists.txt

编辑CMakeLists.txt,添加编译规则:

cmake_minimum_required(VERSION 3.8)
project(obstacle_avoider)

# 设置C++标准
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# 查找依赖包 
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)

# 创建可执行文件 
add_executable(obstacle_avoider_node 
  src/obstacle_avoider_node.cpp
)

# 链接依赖 
ament_target_dependencies(obstacle_avoider_node
  rclcpp
  sensor_msgs
  geometry_msgs
)

# 安装可执行文件 
install(TARGETS
  obstacle_avoider_node
  DESTINATION lib/${PROJECT_NAME}
)

# 如果之后要添加launch文件,可以取消下面的注释
# install(DIRECTORY
#   launch
#   DESTINATION share/${PROJECT_NAME}
# )

# 包结束
ament_package()

📦 第四步:编译和运行

4.1 编译包

cd ~/ros2_ws
colcon build --packages-select obstacle_avoider
source install/setup.bash

4.2 运行测试(需要模拟器或真实机器人)

# 如果你有Gazebo仿真或真实机器人,先启动它们
# 然后运行我们的节点
ros2 run obstacle_avoider obstacle_avoider_node

4.3 观察结果

# 新开终端,查看发布的速度指令
ros2 topic echo /cmd_vel

# 查看节点连接图
rqt_graph

🧠 深入理解:这段C++代码教会了你什么?

1. ROS 2 C++节点的标准结构

  • 继承 rclcpp::Node
  • 构造函数中创建发布者和订阅者
  • 回调函数处理收到的消息
  • main函数三件套:init() -> spin() -> shutdown()

2. 消息类型的使用

  • sensor_msgs::msg::LaserScan:激光雷达数据
  • geometry_msgs::msg::Twist:速度指令

3. 智能避障算法

  • 将激光数据分区处理
  • 基于区域最小距离做决策
  • 实现了"哪边空往哪边转"的直觉逻辑

4. C++特性在ROS中的应用

  • 智能指针SharedPtr自动管理内存
  • std::bind:绑定成员函数作为回调
  • 命名空间:避免命名冲突

🎯 下一步进阶挑战

  1. 添加参数:让安全距离、速度等可动态配置
  2. 改进算法:用动态窗口法(DWA)或向量场直方图(VFH)
  3. 添加服务:实现一个"暂停/继续"避障的服务
  4. 集成导航栈:与Nav2结合,实现更复杂的导航

现在,你已经掌握了ROS 2 C++开发的核心流程!这个避障节点虽然简单,但包含了实际机器人开发中80%的常用模式。有任何不理解的地方,随时问我!

什么是标准消息类型

📦 标准消息类型:ROS世界的“通用语言”

一、先来个超形象的比喻

想象一下这样的场景:

你是中国人,我是美国人。你想告诉我"前方有障碍物",但你不懂英语,我不懂中文。我们怎么交流?

解决方案:我们都用世界语(Esperanto)交流。这是一种人造语言,规则简单,全世界都学它。

在ROS世界里:

  • 你的节点(Node)可能用C++写的(相当于中国人)
  • 我的节点可能用Python写的(相当于美国人)
  • 我们需要交换数据(比如激光雷达数据、速度指令)
  • 标准消息类型就是那个“世界语”!

二、什么是标准消息类型?

标准消息类型是ROS预定义好的、结构化的数据格式,就像一张张表格,规定了:

  • 这个表格叫什么名字(消息类型名)
  • 表格里有哪些列(字段)
  • 每列是什么类型(整型、浮点型、布尔型…)

三、最常见的标准消息类型(重点!)

让我列出你工作中一定会用到的几个:

1. std_msgs - 基础类型
# Bool: 布尔值
bool data

# Int32: 32位整数
int32 data

# Float32: 32位浮点数
float32 data

# String: 字符串
string data
2. geometry_msgs - 几何相关(最常用!)
# Twist: 速度指令(控制机器人移动)
Vector3  linear    # 线速度 (x向前, y向左, z向上)
Vector3  angular   # 角速度 (x滚动, y俯仰, z偏航)

# Pose: 位置和姿态(机器人在哪里)
Point     position    # x, y, z坐标
Quaternion orientation # 朝向(四元数)

# Point: 三维空间中的一个点
float64 x
float64 y
float64 z
3. sensor_msgs - 传感器数据
# LaserScan: 激光雷达数据
float32[] ranges        # 距离数组
float32 angle_min       # 起始角度
float32 angle_max       # 结束角度
float32 range_min       # 最小有效距离
float32 range_max       # 最大有效距离

# Image: 图像数据
uint8[] data           # 像素数据
uint32 height          # 图像高度
uint32 width           # 图像宽度
string encoding        # 编码格式(RGB、灰度等)

# Imu: 惯性测量单元数据
float64[] orientation  # 朝向
float64[] angular_velocity # 角速度
float64[] linear_acceleration # 线加速度
4. nav_msgs - 导航相关
# Odometry: 里程计数据(机器人位置估计)
Header header          # 时间戳和坐标系
PoseWithCovariance pose    # 位置估计
TwistWithCovariance twist  # 速度估计

# Path: 路径(一系列点)
Header header
PoseStamped[] poses    # 路径上的点

四、为什么需要标准消息类型?

场景1:不用标准消息会怎样?
// ❌ 坏代码:没有标准消息
// 你的激光雷达节点:
void publish_my_laser() {
    send_data(1.2, 2.3, 3.4, 5.6); // 这4个数代表什么?谁记得?
}

// 我的避障节点要读数据:
void callback(float a, float b, float c, float d) {
    // a是距离?b是角度?完全不知道!
    // 得去看你的文档...
}
场景2:用了标准消息
// ✅ 好代码:用标准消息
#include <sensor_msgs/msg/laser_scan.hpp>

// 你的激光雷达节点:
void publish_my_laser() {
    sensor_msgs::msg::LaserScan msg;
    msg.ranges = {1.2, 2.3, 3.4, 5.6};  // 一看就知道是距离数组
    msg.angle_min = -1.57;                // 一看就知道是最小角度
    publisher_->publish(msg);
}

// 我的避障节点:
void callback(const sensor_msgs::msg::LaserScan::SharedPtr msg) {
    // 不用看文档,直接就知道怎么用!
    float front_distance = msg->ranges[msg->ranges.size()/2];
    float left_distance = msg->ranges[0];
    float right_distance = msg->ranges[msg->ranges.size()-1];
}

五、标准消息类型的三层结构

ROS消息类型其实是层层嵌套的,就像俄罗斯套娃:

Twist消息类型

linear字段
类型:Vector3

angular字段
类型:Vector3

x: float64

y: float64

z: float64

x: float64

y: float64

z: float64

在代码中看这个结构:

# 查看Twist消息的完整定义
ros2 interface show geometry_msgs/msg/Twist

输出:

# 线速度
Vector3  linear
# 角速度  
Vector3  angular

# 而Vector3的定义是:
float64 x
float64 y
float64 z

六、如何查找和查看标准消息

# 1. 列出所有可用的消息类型
ros2 interface list

# 2. 查看特定消息的详细结构
ros2 interface show geometry_msgs/msg/Twist
ros2 interface show sensor_msgs/msg/LaserScan

# 3. 在代码中自动补全(VS Code + ROS插件)
msg.  # 按Tab,会弹出所有可用的字段!

七、实际应用:完整的数据流

看一个真实机器人工作的数据流:

原始二进制数据

封装成
sensor_msgs/LaserScan

发布
geometry_msgs/Twist

激光雷达硬件

驱动程序节点

/scan话题

避障节点

SLAM建图节点

可视化节点
RViz

/cmd_vel话题

电机控制节点

八、什么时候用标准,什么时候自定义?

场景 用标准还是自定义 原因
控制机器人移动 标准Twist 所有机器人驱动都认这个
发布激光数据 标准LaserScan RViz、导航栈都直接支持
发布图像 标准Image 所有视觉工具都兼容
发布自定义传感器数据 自定义 比如发布“电池电量+温度+湿度”

自定义消息的创建方法(进阶):

# 在你的包中创建 msg/MySensor.msg
float32 battery_voltage
float32 temperature
float32 humidity

九、总结:记住这三点

  1. 标准消息 = ROS世界的普通话:不管节点用什么语言写,都能互相理解
  2. 常用消息要背下来Twist(速度)、LaserScan(激光)、Image(图像)
  3. 消息可以嵌套:大消息由小消息组成,像积木一样

现在回到你之前的避障代码,再看一眼:

// 这是标准消息
sensor_msgs::msg::LaserScan::SharedPtr msg

// 你可以直接这样用
float front = msg->ranges[msg->ranges.size()/2];  // 标准消息保证一定有ranges数组

这就是标准消息的威力:不用看文档,不用猜,一看名字就知道怎么用!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐