ROS 快速入门基础使用教程(1)
ROS快速入门基础教程摘要 ROS是一个机器人开发框架,核心采用发布/订阅通信模式。发布者(如传感器节点)将数据发送到指定话题(Topic),订阅者(如控制节点)从话题接收数据,实现解耦通信。ROS包含三大核心要素:节点(独立进程)、话题(异步通信)和服务(同步请求响应)。ROS 2正成为工业应用主流,C++用于高性能模块,Python适合算法原型开发。典型应用包括自动驾驶、工业机械臂等,开发流程
ROS 快速入门基础使用教程
-
- 什么是发布和订阅
- 用C++实现一个智能避障节点
- 📋 准备工作:理解我们要做什么
- 🚀 第一步:创建C++包
- 📝 第二步:编写智能避障节点
- ⚙️ 第三步:配置CMakeLists.txt
- 📦 第四步:编译和运行
- 🧠 深入理解:这段C++代码教会了你什么?
- 🎯 下一步进阶挑战
- 什么是标准消息类型
- 📦 **标准消息类型: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 1通信的核心流程,我们可以把它总结为三个清晰的步骤 :
-
第一步:登记注册 (步骤1 & 2)
每个节点启动后,第一件事就是去找一个叫ROS Master的特殊节点(你可以把它想象成一个勤劳的“媒婆”)。发布者告诉媒婆:“我要发布话题A!”;订阅者也告诉媒婆:“我对话题A感兴趣!”。 -
第二步:牵线搭桥 (步骤3)
媒婆在自己的小本本上一查,发现发布者和订阅者对上了眼。于是,她偷偷把发布者的“家庭住址”(IP地址和端口号)告诉了订阅者 。 -
第三步:点对点通信 (步骤4-6)
最关键的一步来了:媒婆完成任务后就“功成身退”了!真正的数据传输,是订阅者拿着地址,直接去找发布者建立连接(通常是TCP连接),然后发布者就把数据源源不断地发给订阅者 。
这个设计极其精妙:媒婆(Master)只负责建立连接,不负责传数据。这就避免了中心节点成为整个系统的瓶颈。哪怕媒婆挂了,已经建立好的连接依然可以正常工作!
四、 总结与展望
所以,让我们来回顾一下:
- 发布/订阅 就像编辑部,通过一个“话题篮子”将“记者”和“编辑”解耦,让他们各自专注自己的工作。
- 这么设计的原因,是为了实现 “软件复用” 的伟大理想,让全世界的开发者能像玩乐高一样拼装机器人 。
- ROS本身 不是操作系统,而是一个运行在操作系统上的 “机器人开发框架”,它提供了通信的“神经系统”和大量的“万能工具” 。
- 技术实现上,依靠一个临时的“媒婆”(Master)牵线,之后让节点之间直接“谈恋爱”(点对点通信),保证了系统的高效和健壮 。
现在, 从思想到技术,对ROS有了一个全方位的认识。ROS 2在ROS 1的基础上,把这个“媒婆”去掉了,换成了更先进的“自动发现”机制(基于DDS技术),让系统更加健壮、实时性更好,能适应更多工业级场景 。但万变不离其宗,“发布/订阅”这个核心思想,以及“节点、话题、消息”这些核心概念,是你在ROS世界里永远最趁手的兵器。
ROS 不仅仅是“发消息”,而是一个机器人开发框架,它的核心思想是节点(Node)之间的通信。
ROS 核心概念(快速理解)
- 核心三要素
-
节点(Node):独立运行的进程(程序),例如:传感器驱动、运动控制、算法模块
-
话题(Topic):异步通信方式,一方发布(publish),多方订阅(subscribe) ← 你说的“发消息”主要在这里
-
服务(Service):同步请求-响应模式,像函数调用
-
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的方案来制造用于火星探测器上的镜头,解决了旧系统的延迟和不平滑问题 。
-
公司主要用C++还是Python?
答案是:两个都用,但各有分工。- C++是“心脏”:用于底层控制、驱动、实时性要求高的模块。比如机器人关节的伺服控制、运动规划的核心算法。因为C++性能高、延迟低 。
- Python是“大脑”:用于高层逻辑、算法原型验证、AI集成、脚本编写。比如调用摄像头识别物体、决定机器人下一步该干什么、与用户交互 。
- 你的策略:用Python快速上手和理解流程,然后根据需要,把性能瓶颈部分用C++重写。
-
ROS的典型应用场景和开发任务是什么?
- 典型场景:自动驾驶、工业机械臂(如焊接、抛光、分拣)、仓储移动机器人(AMR)、服务机器人(如酒店送餐)、无人机巡检等 。
- 核心开发任务:“让机器人‘看见’环境,并做出‘反应’”。这背后就是感知 -> 规划 -> 控制的闭环。而ROS 2的核心作用,就是把实现这三个环节的不同程序(节点)连接起来,让它们高效通信。
实战任务:打造一台能“思考”的自动避障机器人
为了让任务贴近真实,我们不再用那个只会“Hello World”的发消息例子。我们将打造一个自主避障机器人。这是几乎所有移动机器人的基础功能。
任务目标
编写两个程序(ROS 2节点):
- 传感器节点:模拟一个激光雷达,不断发布“前方有障碍物!”的警告。
- 控制节点:接收这个警告,并发出“刹车”或“转弯”的命令。
核心要素图解
在你写代码前,先记住这张图,这是ROS 2的“灵魂”:
- 节点:每个独立运行的程序(如我们的
sensor_node和control_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的服务节点。在实际项目中,会有一个专门的节点来记录日志或监控状态。
运行和观察
- 编译工作空间:
cd ~/ros2_ws colcon build source install/setup.bash - 打开三个终端,分别运行:
- 终端1:
ros2 run my_robot_brain sensor_node - 终端2:
ros2 run my_robot_brain control_node - 终端3:
ros2 topic echo /cmd_vel(这个命令可以让你实时看到控制节点发出的速度指令)
- 终端1:
你会看到控制节点根据传感器节点的警告,在“前进”和“刹车”之间切换。你还可以用以下命令“透视”整个系统:
ros2 node list:查看当前运行的节点。ros2 topic list:查看当前所有的通信话题。ros2 topic info /obstacle_warning:查看这个话题的发布者和订阅者信息。rqt_graph:这个命令会打开一个图形化界面,非常直观地显示所有节点和它们之间的通信连接图。快去试试!
总结与下一步
通过这个实战,你已经亲手实践了ROS 2最核心的要素:
- 节点:你写了两个独立的程序(
sensor_node和control_node)。 - 话题:
obstacle_warning和/cmd_vel,实现了传感器和控制器、控制器和硬件之间的广播式通信。 - 服务:
/log_safety_event,实现了一问一答的远程调用。
这就是机器人公司里使用最多的部分:将复杂的机器人功能(感知、规划、控制、硬件驱动)拆分成多个节点,然后用话题和服务将它们组合成一个智能系统。你现在已经具备了看懂和编写大多数ROS 2基础功能的能力。
下一步,可以尝试将sensor_node改为读取一个真实的激光雷达数据(比如/scan话题),并实现一个更智能的避障算法(比如哪边空就往哪边转)。这就进入真正的机器人开发了!
用C++实现一个智能避障节点
非常好!从Python转到C++是走向专业ROS 2开发的必经之路。我会带你从零开始,用C++实现一个智能避障节点,并解释每一步在做什么。
📋 准备工作:理解我们要做什么
我们将创建一个节点,它:
- 订阅激光雷达数据(
/scan话题) - 分析前方左右两侧的障碍物距离
- 决策:哪边空就往哪边转,实现智能避障
- 发布速度指令(
/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:绑定成员函数作为回调
- 命名空间:避免命名冲突
🎯 下一步进阶挑战
- 添加参数:让安全距离、速度等可动态配置
- 改进算法:用动态窗口法(DWA)或向量场直方图(VFH)
- 添加服务:实现一个"暂停/继续"避障的服务
- 集成导航栈:与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消息的完整定义
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,会弹出所有可用的字段!
七、实际应用:完整的数据流
看一个真实机器人工作的数据流:
八、什么时候用标准,什么时候自定义?
| 场景 | 用标准还是自定义 | 原因 |
|---|---|---|
| 控制机器人移动 | ✅ 标准:Twist |
所有机器人驱动都认这个 |
| 发布激光数据 | ✅ 标准:LaserScan |
RViz、导航栈都直接支持 |
| 发布图像 | ✅ 标准:Image |
所有视觉工具都兼容 |
| 发布自定义传感器数据 | ❌ 自定义 | 比如发布“电池电量+温度+湿度” |
自定义消息的创建方法(进阶):
# 在你的包中创建 msg/MySensor.msg
float32 battery_voltage
float32 temperature
float32 humidity
九、总结:记住这三点
- 标准消息 = ROS世界的普通话:不管节点用什么语言写,都能互相理解
- 常用消息要背下来:
Twist(速度)、LaserScan(激光)、Image(图像) - 消息可以嵌套:大消息由小消息组成,像积木一样
现在回到你之前的避障代码,再看一眼:
// 这是标准消息
sensor_msgs::msg::LaserScan::SharedPtr msg
// 你可以直接这样用
float front = msg->ranges[msg->ranges.size()/2]; // 标准消息保证一定有ranges数组
这就是标准消息的威力:不用看文档,不用猜,一看名字就知道怎么用!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐




所有评论(0)