教程说明
本教程基于 ROS2 教程网站 的六章大纲结构,在每一个章节中加入零基础友好的概念解释、生活类比和分步操作指南。建议搭配 Ubuntu 22.04 + ROS2 Humble 环境动手练习。
第一章 ROS2 概述与环境搭建
1.1 什么是 ROS2
ROS2 全称 Robot Operating System 2,是新一代的机器人软件开发框架。它并不是一个真正的操作系统,而是一套"中间件 + 工具链 + 库"的集合。你可以把它理解为机器人世界的 Android——Android 让手机开发者不用关心具体的硬件型号,ROS2 让机器人开发者不用从零开始写通信、驱动、仿真这些底层代码。
ROS2 提供的核心能力包括:进程间通信(节点之间交换数据)、硬件抽象(统一传感器接口)、包管理(代码组织与复用)、可视化与调试工具(rqt、rviz2)。
ROS1 vs ROS2 对比
| 特性 | ROS1 | ROS2 |
|---|---|---|
| 通信中间件 | 自研 TCPROS/UDPROS | DDS(工业标准) |
| Master 节点 | 需要 rosmaster 中央管控 | 去中心化,无需 Master |
| 实时性 | 不支持 | 支持(可运行在 RTOS 上) |
| 多平台 | 仅 Linux | Linux / Windows / macOS |
| 多机器人 | 困难 | 原生支持(Domain ID 隔离) |
| 安全机制 | 无 | DDS-Security(SROS2) |
| Python 版本 | Python 2(已淘汰) | Python 3 |
版本选择
| 版本代号 | 发布时间 | 对应 Ubuntu | 支持周期 |
|---|---|---|---|
| Foxy Fitzroy | 2020.06 | 20.04 | EOL |
| Humble Hawksbill | 2022.05 | 22.04 | LTS 至 2027 |
| Jazzy Jalisco | 2024.05 | 24.04 | LTS 至 2029 |
| Rolling | 滚动 | 最新 | 开发版 |
建议:初学者选择 Humble(搭配 Ubuntu 22.04)或 Jazzy(搭配 Ubuntu 24.04),LTS 版本资料最多、社区支持最完善。
1.2 ROS2 安装(Ubuntu 22.04 + Humble)
1.2.1 设置语言环境
sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
1.2.2 添加 ROS2 软件源
# 安装必要工具
sudo apt update && sudo apt install curl gnupg lsb-release
# 下载 ROS 官方 GPG 公钥(用于验证软件包合法性)
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
-o /usr/share/keyrings/ros-archive-keyring.gpg
# 添加 ROS2 软件源
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
1.2.3 安装 ROS2
sudo apt update
sudo apt upgrade
# 安装桌面完整版(包含 rviz2、rqt 等工具,推荐新手安装这个)
sudo apt install ros-humble-desktop
# 将环境变量写入 .bashrc,每次打开终端自动加载
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source ~/.bashrc
1.2.4 验证安装
打开两个终端:
# 终端1
ros2 run demo_nodes_cpp talker
# 终端2
ros2 run demo_nodes_cpp listener
如果 listener 终端输出接收到的消息,说明安装成功。
1.2.5 安装常用工具
sudo apt install ros-humble-rviz2 # 3D 可视化
sudo apt install ros-humble-rqt* # 图形化工具集
sudo apt install ros-humble-gazebo-ros-pkgs # Gazebo 仿真
sudo apt install ros-humble-tf2-tools # 坐标变换工具
sudo apt install python3-colcon-common-extensions # colcon 编译工具
1.3 ROS2 基本概念(小白必读)
在写代码之前,你需要理解 ROS2 的几个核心概念。用一个"公司"的比喻来帮助理解:
节点(Node)—— 公司里的员工
节点是 ROS2 中最小的可执行单元。每个节点是一个独立的进程,负责一项具体功能。比如:一个节点控制电机、一个节点读取雷达、一个节点做路径规划。
传感器节点 → 感知节点 → 决策节点 → 控制节点 → 执行器节点
常用命令:
ros2 node list # 查看当前运行的所有节点
ros2 node info /节点名 # 查看节点详细信息
工作空间(Workspace)—— 公司大楼
工作空间是你存放所有代码项目的根目录,结构如下:
ros2_ws/ # 工作空间根目录
├── src/ # 源码目录(所有功能包放在这里)
│ ├── my_package_1/ # 功能包1
│ └── my_package_2/ # 功能包2
├── build/ # 编译中间文件(自动生成)
├── install/ # 安装目录(自动生成)
└── log/ # 日志(自动生成)
功能包(Package)—— 员工的工位
功能包是 ROS2 代码组织的基本单位,你可以把它理解为"一个独立的功能模块"。创建功能包:
cd ~/ros2_ws/src
# C++ 功能包
ros2 pkg create my_cpp_pkg --build-type ament_cmake --dependencies rclcpp std_msgs
# Python 功能包
ros2 pkg create my_py_pkg --build-type ament_python --dependencies rclpy std_msgs
编译与运行
cd ~/ros2_ws
# 编译所有包
colcon build
# 只编译指定包(更快)
colcon build --packages-select my_cpp_pkg
# 加载环境(每次开新终端都要执行)
source install/setup.bash
# 运行节点
ros2 run my_cpp_pkg my_node
1.4 四种通信机制总览
这是 ROS2 最核心的部分。用四个生活比喻帮你快速理解:
| 通信方式 | 类比 | 特点 | 适合场景 |
|---|---|---|---|
| 话题 Topic | 广播喇叭 | 持续、单向、多对多 | 传感器数据、键盘遥控 |
| 服务 Service | 打电话 | 一问一答、双向 | 计算距离、查询信息 |
| 动作 Action | 叫外卖 | 持续反馈、可取消、有最终结果 | 导航、机械臂控制 |
| 参数 Parameter | 后台管理面板 | 运行时配置,键值对 | PID 参数、速度阈值 |
详细的通信机制讲解在第二章和第三章,这里先记住:数据持续流用话题、一问一答用服务、长时间任务用动作、改配置用参数。
1.5 ROS2 体系框架
ROS2 从下到上分为三层:
┌────────────────────────────────────┐
│ 应用层 (Your Code) │ ← 你写的功能包
├────────────────────────────────────┤
│ 中间层 (Middleware Layer) │ ← DDS通信 + 客户端库(rclcpp/rclpy)
├────────────────────────────────────┤
│ 操作系统层 (OS Layer) │ ← Linux / Windows / macOS / RTOS
└────────────────────────────────────┘
应用层是你写代码的地方;中间层是 ROS2 帮你处理通信的"基础设施",底层基于 DDS(数据分发服务),你通常不需要直接操作它;操作系统层提供进程管理、网络等基础支持。
作为开发者,你 90% 的时间在应用层工作,偶尔需要理解中间层的概念(比如 QoS 配置)。
第二章 ROS2 通信机制核心
2.1 接口消息(Interface)—— 通信的"语言"
什么是接口?
节点之间通信需要约定"数据格式",这个格式就叫接口。就像两个人通话需要说同一种语言,发布方和订阅方必须使用相同的消息类型。
ROS2 中有三类接口文件:
| 类型 | 扩展名 | 放在哪个目录 | 用途 |
|---|---|---|---|
| 消息 Message | .msg | msg/ | 话题通信的数据格式 |
| 服务 Service | .srv | srv/ | 服务通信的请求+响应格式 |
| 动作 Action | .action | action/ | 动作通信的目标+反馈+结果格式 |
常用内置消息类型
| 包 | 消息类型 | 用途 |
|---|---|---|
std_msgs | String, Int32, Float64, Bool | 基本数据类型 |
geometry_msgs | Twist(速度)、Pose(位姿)、Point、Quaternion | 几何信息 |
sensor_msgs | Image、LaserScan、PointCloud2、Imu | 传感器数据 |
nav_msgs | Odometry、Path、OccupancyGrid | 导航信息 |
查看消息定义:
ros2 interface show geometry_msgs/msg/Twist
2.2 话题通信(Topic)—— 最基础最常用的通信方式
概念详解
话题通信就像校园广播:广播站(发布方)持续播放音乐,收音机(订阅方)调到对应频道就能听到。广播站不管有没有人在听,它只管播;你随时打开随时能听。
核心特征:
- 多对多:一个话题可以有多个发布方和多个订阅方
- 单向流动:数据只从发布方流向订阅方,没有"回复"
- 异步解耦:发布方和订阅方互不认识、互不依赖
- 持续产生:适合传感器数据、控制指令等持续产生的数据
C++ 实现——发布者
// src/simple_publisher.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class SimplePublisher : public rclcpp::Node
{
public:
SimplePublisher() : Node("simple_publisher"), count_(0)
{
// 第一步:创建发布方
// 参数:消息类型、话题名、队列深度
publisher_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
// 第二步:创建定时器,每 500ms 触发一次回调
timer_ = this->create_wall_timer(
500ms, std::bind(&SimplePublisher::timer_callback, this));
}
private:
// 第三步:定时器回调——组装消息并发布
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello ROS2! Count: " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "发布: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<SimplePublisher>());
rclcpp::shutdown();
return 0;
}
C++ 实现——订阅者
// src/simple_subscriber.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class SimpleSubscriber : public rclcpp::Node
{
public:
SimpleSubscriber() : Node("simple_subscriber")
{
// 创建订阅方,绑定回调函数
subscription_ = this->create_subscription<std_msgs::msg::String>(
"chatter", 10,
std::bind(&SimpleSubscriber::topic_callback, this,
std::placeholders::_1));
}
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<SimpleSubscriber>());
rclcpp::shutdown();
return 0;
}
CMakeLists.txt 配置
# 映射源文件与可执行文件
add_executable(simple_publisher src/simple_publisher.cpp)
ament_target_dependencies(simple_publisher rclcpp std_msgs)
add_executable(simple_subscriber src/simple_subscriber.cpp)
ament_target_dependencies(simple_subscriber rclcpp std_msgs)
# 安装
install(TARGETS
simple_publisher
simple_subscriber
DESTINATION lib/${PROJECT_NAME}
)
Python 实现——发布者
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class SimplePublisher(Node):
def __init__(self):
super().__init__('simple_publisher')
self.publisher_ = self.create_publisher(String, 'chatter', 10)
self.timer = self.create_timer(0.5, self.timer_callback)
self.count = 0
def timer_callback(self):
msg = String()
msg.data = f'Hello ROS2! Count: {self.count}'
self.publisher_.publish(msg)
self.get_logger().info(f'发布: "{msg.data}"')
self.count += 1
def main(args=None):
rclpy.init(args=args)
node = SimplePublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
Python 实现——订阅者
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class SimpleSubscriber(Node):
def __init__(self):
super().__init__('simple_subscriber')
self.subscription = self.create_subscription(
String, 'chatter', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'收到: "{msg.data}"')
def main(args=None):
rclpy.init(args=args)
node = SimpleSubscriber()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
话题通信命令行调试
# 查看所有话题
ros2 topic list
# 查看话题类型
ros2 topic list -t
# 监听话题消息
ros2 topic echo /chatter
# 命令行发布消息
ros2 topic pub /chatter std_msgs/msg/String "{data: 'Hello from CLI'}"
# 查看话题发布频率
ros2 topic hz /chatter
QoS(服务质量策略)
QoS 控制通信的可靠性,就像快递服务可以选择"次日达"还是"经济件"。
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy
qos_profile = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE, # 保证送达(类似"次日达")
# reliability=ReliabilityPolicy.BEST_EFFORT, # 尽力而为(类似"经济件",更快但可能丢包)
durability=DurabilityPolicy.TRANSIENT_LOCAL, # 新订阅者能收到历史消息
history=HistoryPolicy.KEEP_LAST, # 队列满时丢弃旧消息
depth=10 # 队列深度
)
QoS 兼容性规则(发布方和订阅方必须兼容才能通信):
| 发布者 | 订阅者 | 是否兼容 |
|---|---|---|
| RELIABLE | RELIABLE | 兼容 |
| BEST_EFFORT | BEST_EFFORT | 兼容 |
| RELIABLE | BEST_EFFORT | 兼容 |
| BEST_EFFORT | RELIABLE | 不兼容 |
2.3 服务通信(Service)—— 一问一答
概念详解
服务通信就像打 10086 查话费:你拨出去(发请求),客服帮你查(服务端处理),然后告诉你结果(返回响应)。每次问一个问题、得到一个答案,这通电话就结束了。
核心特征:
- 一对一:一个请求对应一个响应
- 双向:有去有回(Request → Response)
- 同步等待:客户端发出请求后等待结果
- 一次性:不是持续的,问完就结束
定义服务接口 (.srv 文件)
# --- 请求部分(客户端发给服务端的) ---
float32 x
float32 y
---
# --- 响应部分(服务端返回给客户端的) ---
float32 distance
--- 是分隔线,上面是 Request 字段,下面是 Response 字段。
C++ 服务端
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"
class AddTwoIntsServer : public rclcpp::Node
{
public:
AddTwoIntsServer() : Node("add_two_ints_server")
{
// 创建服务:服务类型、服务名、回调函数
// 注意回调函数有两个参数:_1 = request, _2 = response
service_ = this->create_service<example_interfaces::srv::AddTwoInts>(
"add_two_ints",
std::bind(&AddTwoIntsServer::handle_service, this,
std::placeholders::_1, std::placeholders::_2));
}
private:
// 核心:从 request 读数据,往 response 写结果
void handle_service(
const example_interfaces::srv::AddTwoInts::Request::SharedPtr request,
example_interfaces::srv::AddTwoInts::Response::SharedPtr response)
{
response->sum = request->a + request->b;
RCLCPP_INFO(this->get_logger(), "%ld + %ld = %ld",
request->a, request->b, response->sum);
}
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service_;
};
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<AddTwoIntsServer>());
rclcpp::shutdown();
return 0;
}
C++ 客户端
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"
using namespace std::chrono_literals;
class AddTwoIntsClient : public rclcpp::Node
{
public:
AddTwoIntsClient() : Node("add_two_ints_client")
{
client_ = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
// 等待服务上线
while (!client_->wait_for_service(1s)) {
if (!rclcpp::ok()) return;
RCLCPP_INFO(this->get_logger(), "等待服务上线...");
}
// 发送请求
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = 3;
request->b = 5;
auto future = client_->async_send_request(request);
}
private:
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_;
};
服务通信命令行调试
# 查看服务列表
ros2 service list
# 命令行调用服务
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 3, b: 5}"
2.4 动作通信(Action)—— 长时间任务的利器
概念详解
动作通信就像叫外卖:你下单(发送 Goal),骑手持续更新位置给你看(连续 Feedback),送到了确认完成(最终 Result),等太久你还可以取消订单(Cancel)。
核心特征:
- 请求-反馈-结果三段式
- 可取消:客户端随时可以取消正在执行的任务
- 多线程:执行逻辑在独立线程中运行,不阻塞节点
- 适合长时间任务:导航、机械臂运动规划等
定义动作接口 (.action 文件)
# Goal(目标)
float32 goal_x
float32 goal_y
float32 goal_theta
---
# Result(最终结果)
float32 turtle_x
float32 turtle_y
float32 turtle_theta
---
# Feedback(连续反馈)
float32 distance
两段 --- 把文件分成三段:Goal、Result、Feedback。
动作服务端的三个回调
动作服务端的核心在于三个回调函数的协作:
客户端发送 Goal
↓
handle_goal() ← 门卫:检查目标是否合法,返回 ACCEPT 或 REJECT
↓ (如果接受)
handle_accepted() ← 调度员:创建新线程,把任务交给 execute()
↓
execute() 在新线程中 ← 工人:执行任务,循环中发布 feedback,最后 succeed(result)
期间如果客户端取消:
handle_cancel() ← 确认是否允许取消,返回 ACCEPT
execute() 中检查 is_canceling() → canceled(result) 退出
C++ 动作服务端实现
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "base_interfaces_demo/action/nav.hpp"
#include "turtlesim/msg/pose.hpp"
#include "geometry_msgs/msg/twist.hpp"
using base_interfaces_demo::action::Nav;
using namespace std::placeholders;
class ExeNavActionServer : public rclcpp::Node
{
public:
ExeNavActionServer() : Node("exe_nav_action_server")
{
// 订阅乌龟位姿(实时获取当前位置)
pose_sub = this->create_subscription<turtlesim::msg::Pose>(
"/turtle1/pose", 10,
std::bind(&ExeNavActionServer::poseCallBack, this, _1));
// 创建速度发布方(控制乌龟运动)
cmd_vel_pub = this->create_publisher<geometry_msgs::msg::Twist>("/turtle1/cmd_vel", 10);
// 创建 Action 服务端,绑定三个回调
nav_action_server = rclcpp_action::create_server<Nav>(
this, "nav",
std::bind(&ExeNavActionServer::handle_goal, this, _1, _2),
std::bind(&ExeNavActionServer::handle_cancel, this, _1),
std::bind(&ExeNavActionServer::handle_accepted, this, _1));
}
private:
turtlesim::msg::Pose::SharedPtr turtle1_pose = nullptr;
rclcpp::Subscription<turtlesim::msg::Pose>::SharedPtr pose_sub;
rclcpp_action::Server<Nav>::SharedPtr nav_action_server;
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_vel_pub;
void poseCallBack(const turtlesim::msg::Pose::SharedPtr pose) {
turtle1_pose = pose; // 实时缓存乌龟位置
}
// 回调1:收到目标 → 校验并决定是否接受
rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID &, std::shared_ptr<const Nav::Goal> goal)
{
RCLCPP_INFO(this->get_logger(), "请求坐标:(%.2f,%.2f)", goal->goal_x, goal->goal_y);
if (goal->goal_x < 0 || goal->goal_x > 11.1 ||
goal->goal_y < 0 || goal->goal_y > 11.1)
return rclcpp_action::GoalResponse::REJECT;
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}
// 回调2:收到取消请求
rclcpp_action::CancelResponse handle_cancel(
std::shared_ptr<rclcpp_action::ServerGoalHandle<Nav>>)
{
return rclcpp_action::CancelResponse::ACCEPT;
}
// 回调3:目标被接受 → 启动新线程执行任务
void handle_accepted(std::shared_ptr<rclcpp_action::ServerGoalHandle<Nav>> goal_handle) {
std::thread{std::bind(&ExeNavActionServer::execute, this, _1), goal_handle}.detach();
}
// 核心执行逻辑(在新线程中运行)
void execute(std::shared_ptr<rclcpp_action::ServerGoalHandle<Nav>> goal_handle)
{
float goal_x = goal_handle->get_goal()->goal_x;
float goal_y = goal_handle->get_goal()->goal_y;
auto feedback = std::make_shared<Nav::Feedback>();
auto result = std::make_shared<Nav::Result>();
rclcpp::Rate rate(1.0); // 每秒执行一次
while (true) {
// 检查是否被取消
if (goal_handle->is_canceling()) {
goal_handle->canceled(result);
return;
}
// 计算偏差
float x_dist = goal_x - turtle1_pose->x;
float y_dist = goal_y - turtle1_pose->y;
// 比例控制:速度 = 0.5 × 偏差(离目标越远速度越快)
geometry_msgs::msg::Twist twist;
twist.linear.x = 0.5 * x_dist;
twist.linear.y = 0.5 * y_dist;
cmd_vel_pub->publish(twist);
// 计算剩余距离
float distance = sqrt(pow(x_dist, 2) + pow(y_dist, 2));
// 到达判断
if (distance < 0.15) {
result->turtle_x = turtle1_pose->x;
result->turtle_y = turtle1_pose->y;
result->turtle_theta = turtle1_pose->theta;
break;
}
// 发送连续反馈
feedback->distance = distance;
goal_handle->publish_feedback(feedback);
rate.sleep();
}
goal_handle->succeed(result); // 任务成功完成
}
};
2.5 参数(Parameter)—— 运行时配置
概念详解
参数是节点的运行时配置值,就像你手机设置里的"亮度"、“音量”——不需要重启 App 就能调整。底层实现其实是基于服务通信的(每个声明了参数的节点自动创建了一组内置服务)。
支持的参数类型
bool、int、double、string、以及它们的数组类型。
C++ 参数使用
#include "rclcpp/rclcpp.hpp"
class ParamDemoNode : public rclcpp::Node
{
public:
ParamDemoNode() : Node("param_demo")
{
// 声明参数(名称 + 默认值)
this->declare_parameter("robot_name", "my_robot");
this->declare_parameter("max_speed", 1.0);
// 读取参数
std::string name = this->get_parameter("robot_name").as_string();
double speed = this->get_parameter("max_speed").as_double();
RCLCPP_INFO(this->get_logger(), "Robot: %s, Speed: %.2f", name.c_str(), speed);
// 注册变更回调(参数被外部修改时触发)
param_callback_handle_ = this->add_on_set_parameters_callback(
std::bind(&ParamDemoNode::on_change, this, std::placeholders::_1));
}
private:
rcl_interfaces::msg::SetParametersResult on_change(
const std::vector<rclcpp::Parameter> ¶ms)
{
rcl_interfaces::msg::SetParametersResult result;
result.successful = true;
for (const auto &p : params) {
RCLCPP_INFO(this->get_logger(), "参数 %s 变为 %s",
p.get_name().c_str(), p.value_to_string().c_str());
// 可以在这里做校验,比如 max_speed 不能超过 10
}
return result;
}
rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr param_callback_handle_;
};
命令行操作参数
# 列出节点所有参数
ros2 param list /param_demo
# 读取参数
ros2 param get /param_demo robot_name
# 运行时修改参数(会触发 on_change 回调)
ros2 param set /param_demo max_speed 2.5
# 导出参数到 YAML
ros2 param dump /param_demo > params.yaml
# 从 YAML 加载参数启动节点
ros2 run my_pkg my_node --ros-args --params-file params.yaml
YAML 参数文件格式
param_demo:
ros__parameters:
robot_name: "super_robot"
max_speed: 2.0
pid:
kp: 1.0
ki: 0.1
kd: 0.01
2.6 通信方式选择指南
拿到一个需求时,问自己两个问题:
Q1:数据是持续产生的,还是一次性的?
- 持续的 → 话题(传感器数据、键盘控制)
- 一次性的 → 进入 Q2
Q2:这个任务需要较长时间才能完成吗?过程中需要汇报进度吗?
- 不需要,一问一答 → 服务(计算距离、切换模式)
- 需要,或者需要支持取消 → 动作(导航、机械臂运动)
- 只是改配置 → 参数(调 PID、改阈值)
实际项目中四种方式经常组合使用,比如一个动作服务端内部可能同时用话题来订阅传感器数据和发布控制指令。
第三章 ROS2 通信机制补充
3.1 功能包与接口详解
接口包创建
自定义接口通常放在单独的功能包中:
cd ~/ros2_ws/src
ros2 pkg create my_interfaces --build-type ament_cmake
cd my_interfaces
mkdir msg srv action
自定义消息
# msg/RobotStatus.msg
string robot_name
float64 battery_level # 电池电量 0.0~1.0
float64 temperature # 温度
bool is_emergency # 是否紧急状态
int32 error_code # 错误码
geometry_msgs/Twist velocity # 引用其他包的消息
float64[] joint_positions # 数组类型
接口包 CMakeLists.txt
find_package(rosidl_default_generators REQUIRED)
find_package(geometry_msgs REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/RobotStatus.msg"
"srv/GetRobotStatus.srv"
"action/Navigate.action"
DEPENDENCIES geometry_msgs
)
接口包 package.xml
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<depend>geometry_msgs</depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
编译后在代码中使用
// C++
#include "my_interfaces/msg/robot_status.hpp"
#include "my_interfaces/srv/get_robot_status.hpp"
#include "my_interfaces/action/navigate.hpp"
# Python
from my_interfaces.msg import RobotStatus
from my_interfaces.srv import GetRobotStatus
from my_interfaces.action import Navigate
3.2 话题通信练习——两只乌龟镜像运动
这个练习的目标:打开两个 turtlesim 窗口,你通过键盘控制第一只乌龟,第二只乌龟会"镜像"第一只的速度(角速度取反)。
节点代码
#include <rclcpp/rclcpp.hpp>
#include <turtlesim/msg/pose.hpp>
#include <geometry_msgs/msg/twist.hpp>
class ExePubSub : public rclcpp::Node {
public:
ExePubSub() : rclcpp::Node("demo01_pub_sub") {
// 发布方:控制第二只乌龟(在 t2 命名空间下)
twist_pub_ = this->create_publisher<geometry_msgs::msg::Twist>(
"/t2/turtle1/cmd_vel", 1);
// 订阅方:监听第一只乌龟的位姿
pose_sub_ = this->create_subscription<turtlesim::msg::Pose>(
"/turtle1/pose", 1,
std::bind(&ExePubSub::poseCallback, this, std::placeholders::_1));
}
private:
void poseCallback(const turtlesim::msg::Pose::ConstSharedPtr pose) {
geometry_msgs::msg::Twist twist;
twist.angular.z = -(pose->angular_velocity); // 角速度取反 → 镜像效果
twist.linear.x = pose->linear_velocity; // 线速度不变
twist_pub_->publish(twist);
}
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr twist_pub_;
rclcpp::Subscription<turtlesim::msg::Pose>::SharedPtr pose_sub_;
};
Launch 文件
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import ExecuteProcess, RegisterEventHandler
from launch.event_handlers import OnProcessExit
def generate_launch_description():
t1 = Node(package="turtlesim", executable="turtlesim_node")
t2 = Node(package="turtlesim", executable="turtlesim_node", namespace="t2")
# 让第二只乌龟掉头 180 度
rotate = ExecuteProcess(
cmd=["ros2 action send_goal /t2/turtle1/rotate_absolute "
"turtlesim/action/RotateAbsolute \"{'theta': 3.14}\""],
output="both", shell=True)
pub_sub = Node(package="cpp07_exercise", executable="exe01_pub_sub")
# 掉头完毕后再启动控制节点
rotate_exit_event = RegisterEventHandler(
event_handler=OnProcessExit(target_action=rotate, on_exit=pub_sub))
return LaunchDescription([t1, t2, rotate, rotate_exit_event])
数据流图
键盘 → /turtle1/cmd_vel → 第一只乌龟运动
↓
/turtle1/pose(位姿发布)
↓
ExePubSub 节点(角速度取反)
↓
/t2/turtle1/cmd_vel → 第二只乌龟镜像运动
3.3 服务通信练习——计算乌龟到目标点的距离
(代码详解见前文,此处给出完整流程)
步骤
- 定义
Distance.srv(请求:x, y, theta → 响应:distance) - 编写服务端:订阅乌龟位姿缓存当前位置,服务回调中用勾股定理计算距离
- 编写客户端:从命令行参数读取目标坐标,发送请求,等待结果
- 配置 CMakeLists.txt 编译运行
关键代码对比
服务端回调:
request → 读取目标坐标
成员变量 → 读取乌龟当前位置(由位姿订阅回调实时更新)
计算 distance = sqrt((目标x-当前x)² + (目标y-当前y)²)
response → 返回 distance
第四章 Launch 文件与 rosbag2
4.1 Launch 文件——零基础入门
为什么需要 Launch?
一个机器人系统通常由多个节点组成(雷达节点、相机节点、导航节点……)。如果每次都要手动一个终端一个终端地 ros2 run,非常麻烦。Launch 文件就是一个**“一键启动脚本”**——一行命令同时启动所有节点,还能配置参数、控制启动顺序。
没有 Launch:
# 终端1: ros2 run turtlesim turtlesim_node
# 终端2: ros2 run turtlesim turtle_teleop_key
# 终端3: ros2 run my_package my_controller
# ...开到第8个终端你已经疯了
有 Launch:
# 只需要一行命令
ros2 launch my_package my_robot.launch.py
基本骨架
Launch 文件是 Python 脚本(.launch.py 后缀),骨架非常固定:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
# 在这里定义你要启动的东西
# 最后打包成 LaunchDescription 返回
node1 = Node(package="包名", executable="可执行文件名")
node2 = Node(package="包名", executable="可执行文件名")
return LaunchDescription([node1, node2])
Node 的常用参数
my_node = Node(
package="my_package", # 功能包名(必填)
executable="my_node", # 可执行文件名(必填)
name="custom_name", # 自定义节点名
namespace="robot1", # 命名空间(隔离话题名)
output="screen", # 输出到终端(默认只写日志)
parameters=[{"max_speed": 3.0}], # 传入参数
arguments=["arg1", "arg2"], # 命令行参数
remappings=[ # 话题重映射
("/old_topic", "/new_topic")
]
)
命名空间的作用:给话题名加前缀,防止冲突。比如两个 turtlesim 节点的话题都叫 /turtle1/pose,加上 namespace="t2" 后第二个就变成了 /t2/turtle1/pose。
话题重映射的作用:不改代码就能改变数据流向。节点代码里写的是 /turtle1/cmd_vel,重映射后实际收发的是 /my_robot/velocity。
执行外部命令
from launch.actions import ExecuteProcess
spawn_turtle = ExecuteProcess(
cmd=["ros2", "service", "call", "/spawn",
"turtlesim/srv/Spawn",
"'{x: 5.0, y: 5.0, theta: 0.0, name: \"turtle2\"}'"],
output="both",
shell=True
)
运行时传参
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
# 声明参数(带默认值)
speed_arg = DeclareLaunchArgument("speed", default_value="2.0")
# 在节点中使用
my_node = Node(
package="my_pkg", executable="my_node",
parameters=[{"max_speed": LaunchConfiguration("speed")}]
)
return LaunchDescription([speed_arg, my_node])
运行时覆盖:
ros2 launch my_pkg demo.launch.py speed:=5.0
事件驱动——控制启动顺序
from launch.actions import RegisterEventHandler
from launch.event_handlers import OnProcessExit
# 当 rotate 进程退出后,才启动 controller
rotate_done = RegisterEventHandler(
OnProcessExit(target_action=rotate, on_exit=controller)
)
条件启动
from launch.conditions import IfCondition
Node(
package='rviz2', executable='rviz2',
condition=IfCondition(LaunchConfiguration('use_rviz'))
)
包含其他 Launch 文件
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
other_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
PathJoinSubstitution([
FindPackageShare("other_pkg"), "launch", "other.launch.py"
])
]),
launch_arguments={"param1": "value1"}.items()
)
安装 Launch 文件
在 CMakeLists.txt 中:
install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})
4.2 rosbag2——录包与回放
什么是 rosbag?
rosbag 就像一个"录像机",它可以录制 ROS2 话题上流动的所有消息数据,之后可以回放。这在调试和复现问题时非常有用——你不需要让机器人在外面跑一遍又一遍,只要录一次数据,回来慢慢分析。
录制
# 录制指定话题
ros2 bag record /topic1 /topic2
# 录制所有话题
ros2 bag record -a
# 指定输出名称
ros2 bag record -a -o my_bag
# 最长录制 60 秒
ros2 bag record -a --max-bag-duration 60
# 压缩录制
ros2 bag record /scan --compression-mode file --compression-format zstd
查看与回放
# 查看录包信息
ros2 bag info my_bag
# 回放
ros2 bag play my_bag
# 2 倍速回放
ros2 bag play my_bag --rate 2.0
# 循环播放
ros2 bag play my_bag --loop
# 只回放指定话题
ros2 bag play my_bag --topics /scan /odom
第五章 坐标变换(TF2)
5.1 TF2 是什么?为什么需要它?
生活类比
想象你坐在移动的汽车里,手里拿着一个杯子。杯子相对于你的手是不动的,你的手相对于你的身体是不动的,但你相对于地面是在移动的。如果有人问"杯子在地面坐标系中的位置是哪里?",你就需要做一连串的坐标转换。
机器人也是一样:激光雷达装在车顶上,车顶在地面上移动,地图是以世界原点为基准的。要知道"雷达扫描到的障碍物在地图中的位置",就需要经过多次坐标变换。TF2 就是帮你管理这些变换关系的工具。
TF2 的树状结构
map ← 世界坐标系(固定不动)
│
odom ← 里程计坐标系(随机器人移动累积)
│
base_link ← 机器人本体坐标系
/ \
left_wheel right_wheel ← 轮子坐标系
│
laser_link ← 雷达坐标系
每条边代表一个坐标变换(平移 + 旋转),由 TransformStamped 消息描述。
5.2 核心概念
- TF Broadcaster(广播者):发布坐标变换(“告诉别人我在哪”)
- TF Listener(监听者):查询坐标变换(“问别人他在哪”)
- Static TF(静态变换):不随时间变化的变换(如雷达安装在车顶的固定位置)
- Dynamic TF(动态变换):随时间变化的变换(如机器人在地面上移动)
5.3 静态坐标变换
#include "rclcpp/rclcpp.hpp"
#include "tf2_ros/static_transform_broadcaster.h"
#include "geometry_msgs/msg/transform_stamped.hpp"
#include "tf2/LinearMath/Quaternion.h"
class StaticTFBroadcaster : public rclcpp::Node
{
public:
StaticTFBroadcaster() : Node("static_tf_broadcaster")
{
tf_broadcaster_ = std::make_shared<tf2_ros::StaticTransformBroadcaster>(this);
geometry_msgs::msg::TransformStamped t;
t.header.stamp = this->get_clock()->now();
t.header.frame_id = "base_link"; // 父坐标系
t.child_frame_id = "laser_link"; // 子坐标系
// 平移:雷达在车体前方 0.1m,上方 0.2m
t.transform.translation.x = 0.1;
t.transform.translation.y = 0.0;
t.transform.translation.z = 0.2;
// 旋转:无旋转(用四元数表示)
tf2::Quaternion q;
q.setRPY(0, 0, 0);
t.transform.rotation.x = q.x();
t.transform.rotation.y = q.y();
t.transform.rotation.z = q.z();
t.transform.rotation.w = q.w();
tf_broadcaster_->sendTransform(t);
}
private:
std::shared_ptr<tf2_ros::StaticTransformBroadcaster> tf_broadcaster_;
};
5.4 动态坐标变换
#include "rclcpp/rclcpp.hpp"
#include "tf2_ros/transform_broadcaster.h"
#include "geometry_msgs/msg/transform_stamped.hpp"
#include "tf2/LinearMath/Quaternion.h"
class DynamicTFBroadcaster : public rclcpp::Node
{
public:
DynamicTFBroadcaster() : Node("dynamic_tf_broadcaster")
{
tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(*this);
// 每 100ms 发布一次
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&DynamicTFBroadcaster::broadcast, this));
}
private:
void broadcast()
{
geometry_msgs::msg::TransformStamped t;
t.header.stamp = this->get_clock()->now();
t.header.frame_id = "odom";
t.child_frame_id = "base_link";
// 示例:圆周运动
double sec = this->get_clock()->now().seconds();
t.transform.translation.x = sin(sec);
t.transform.translation.y = cos(sec);
t.transform.translation.z = 0.0;
tf2::Quaternion q;
q.setRPY(0, 0, sec);
t.transform.rotation = tf2::toMsg(q);
tf_broadcaster_->sendTransform(t);
}
std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
rclcpp::TimerBase::SharedPtr timer_;
};
5.5 监听坐标变换
#include "rclcpp/rclcpp.hpp"
#include "tf2_ros/buffer.h"
#include "tf2_ros/transform_listener.h"
class TFListener : public rclcpp::Node
{
public:
TFListener() : Node("tf_listener")
{
tf_buffer_ = std::make_unique<tf2_ros::Buffer>(this->get_clock());
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
std::bind(&TFListener::on_timer, this));
}
private:
void on_timer()
{
try {
// 查询从 base_link 到 laser_link 的变换
auto t = tf_buffer_->lookupTransform("base_link", "laser_link", tf2::TimePointZero);
RCLCPP_INFO(this->get_logger(), "Translation: [%.2f, %.2f, %.2f]",
t.transform.translation.x, t.transform.translation.y, t.transform.translation.z);
} catch (const tf2::TransformException &ex) {
RCLCPP_WARN(this->get_logger(), "变换查询失败: %s", ex.what());
}
}
std::unique_ptr<tf2_ros::Buffer> tf_buffer_;
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
rclcpp::TimerBase::SharedPtr timer_;
};
5.6 TF2 调试工具
# 生成 TF 树可视化 PDF
ros2 run tf2_tools view_frames
# 实时查看两个坐标系之间的变换
ros2 run tf2_ros tf2_echo base_link laser_link
# 监控所有 TF 发布
ros2 run tf2_ros tf2_monitor
第六章 可视化工具(RViz2)
6.1 RViz2 是什么?
RViz2 是 ROS2 内置的三维可视化工具。它可以图形化地显示机器人模型、传感器数据(雷达点云、摄像头图像)、地图、路径规划结果等。你可以把它理解为"机器人的眼睛"——把抽象的数据变成你能看到的画面。
6.2 启动 RViz2
rviz2
或者在 Launch 文件中启动并加载配置:
Node(
package='rviz2',
executable='rviz2',
arguments=['-d', os.path.join(pkg_path, 'rviz', 'config.rviz')],
)
6.3 常用显示插件
| 显示类型 | 用途 |
|---|---|
| TF | 显示坐标变换树 |
| RobotModel | 显示机器人 URDF 模型 |
| LaserScan | 显示激光雷达点云 |
| PointCloud2 | 显示 3D 点云 |
| Image | 显示摄像头图像 |
| Map | 显示占据栅格地图 |
| Path | 显示路径规划结果 |
| Pose | 显示位姿箭头 |
| MarkerArray | 显示自定义标记 |
6.4 rqt 图形化工具集
rqt 是另一套图形化工具,比命令行更直观:
rqt # 主界面(包含所有插件)
rqt_graph # 可视化节点之间的通信关系图
rqt_topic # 监控话题数据
rqt_service_caller # 调用服务
rqt_console # 查看日志
rqt_plot # 数据绘图(实时曲线)
rqt_image_view # 查看摄像头图像
rqt_tf_tree # 可视化 TF 树
rqt_reconfigure # 动态调节参数
新手建议:调试通信关系时用
rqt_graph,看数据波形时用rqt_plot,调参时用rqt_reconfigure,这三个是最常用的。
附录
A. 通用开发流程(所有通信方式共享)
第1步:定义接口(.msg / .srv / .action)
↓
第2步:配置接口包的 CMakeLists.txt,编译生成 C++ 头文件
↓
第3步:编写节点代码(继承 Node 类,创建 pub/sub/server/client)
↓
第4步:配置功能包的 CMakeLists.txt 和 package.xml
↓
第5步:编写 Launch 文件(可选,用于同时启动多个节点)
↓
第6步:colcon build 编译
↓
第7步:source install/setup.bash,ros2 launch 或 ros2 run 运行
B. 常用命令速查
# ===== 环境 =====
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
# ===== 构建 =====
colcon build
colcon build --packages-select <pkg>
colcon build --symlink-install
# ===== 运行 =====
ros2 run <pkg> <exec>
ros2 launch <pkg> <launch_file>
# ===== 信息查询 =====
ros2 node list / info
ros2 topic list / info / echo / hz / pub
ros2 service list / type / call
ros2 action list / info / send_goal
ros2 param list / get / set / dump
ros2 interface list / show
# ===== 调试 =====
rqt_graph
rqt_console
ros2 bag record / play / info
ros2 run tf2_tools view_frames
ros2 run tf2_ros tf2_echo <frame1> <frame2>
C. 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 节点看不到彼此 | Domain ID 不同 | 检查 ROS_DOMAIN_ID |
| 话题订阅收不到消息 | QoS 不匹配 | 检查 Reliability/Durability |
| colcon build 失败 | 缺少依赖 | rosdep install --from-paths src |
| Import 找不到包 | 未 source | source install/setup.bash |
| TF 查询失败 | 变换未发布或超时 | ros2 run tf2_tools view_frames |
D. 推荐学习路径
第1周:安装 + 基础概念 + 话题通信
↓
第2周:服务 + 动作 + 参数 + Launch
↓
第3周:自定义接口 + TF2 坐标变换
↓
第4周:URDF 建模 + Gazebo 仿真
↓
第5-6周:Nav2 导航(SLAM + 定位 + 路径规划)
↓
第7-8周:MoveIt2(如涉及机械臂)
↓
进阶:生命周期节点 + 组件 + DDS 调优 + 实际项目
E. 学习资源
| 资源 | 链接 |
|---|---|
| 官方文档 | https://docs.ros.org/en/humble/ |
| ROS2 中文教程 | https://rzl6.github.io/ROS2_Tuition/ |
| Navigation2 文档 | https://navigation.ros.org/ |
| MoveIt2 文档 | https://moveit.picknik.ai/ |
| 鱼香ROS | https://fishros.org.cn/ |
| 古月居 | https://www.guyuehome.com/ |
9930

被折叠的 条评论
为什么被折叠?



