0x01 重要概念
ROS2本質上是一個通信框架,有兩個重要組成:
- node : 通信對象
- topic : 消息主題
ROS2 三種通信方式
├── Topic(發(fā)布/訂閱) — 適合持續(xù)數(shù)據(jù)流(傳感器、狀態(tài))
├── Service(服務) — 請求/響應模式,適合一次性指令(開關燈、查詢狀態(tài))
└── Action(動作) — 長時任務 + 進度反饋(導航到目標點、機械臂運動)
上面的概念對于后端開發(fā)而言,理解起來很容易。
0x02 DDS協(xié)議
DDS( Data Distribution Service) 分布式通信協(xié)議標準。
0x03 FastDDS
FastDDS: 作為上述協(xié)議的一個實現(xiàn),主要功能是為node提供發(fā)現(xiàn)機制
分兩個重要階段:
一、參與者發(fā)現(xiàn)階段(PDP)
目的:讓不同DomainParticipant相互感知存在。
機制:每個參與者周期性地發(fā)送公告消息(默認通過基于DomainId計算的多播地址和端口),消息中包含接收元數(shù)據(jù)和用戶數(shù)據(jù)流量的單播地址(IP+端口)。
匹配條件:兩個參與者必須在同一個DDS Domain中。
配置選項:可通過“Initial peers”指定單播地址列表發(fā)送公告。
可配置公告的發(fā)送周期(通過發(fā)現(xiàn)配置)。
二、端點發(fā)現(xiàn)階段(EDP)
目的:讓DataWriter和DataReader互相發(fā)現(xiàn)。
機制:參與者利用PDP建立的通信通道,交換各自擁有的DataWriter和DataReader信息,包括Topic和數(shù)據(jù)類型。
匹配條件:Topic和數(shù)據(jù)類型必須一致。
結果:匹配成功后,DataWriter和DataReader即可開始收發(fā)用戶數(shù)據(jù)。
經(jīng)過上面兩個階段后,所有參與者(node) 都會知道相互之間的 topic 和消息數(shù)據(jù)格式了。
0x04 node 發(fā)布流程
一個典型的node發(fā)布消息的調(diào)用棧關系如下:
- RCLCPP層
- rclcpp::Publisher::publish
- rclcpp::Publisher::do_inter_process_publish
- RCL層
- rcl_publish
- RMW層
1.rmw_publish- rmw_fastrtps_shared_cpp::__rmw_publish
- FastDDS層
- eprosima::fastdds::dds::DataWriter::write_w_timestamp
- eprosima::fastdds::dds::DataWriterImpl::write_w_timestamp
- eprosima::fastdds::dds::DataWriterImpl::create_new_change_with_params
4.eprosima::fastdds::dds::DataWriterImpl::perform_create_new_change
0x05 node 訂閱
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
auto topic_callback =
[this](std_msgs::msg::String::UniquePtr msg) -> void {
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
};
subscription_ =
this->create_subscription<std_msgs::msg::String>("topic", 10, topic_callback);
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
0x06 Message
傳遞的消息格式:
std_msgs/Header header
uint32 height
uint32 width
string encoding
uint8 is_bigendian
uint32 step
uint8[] data
0x07 完成代碼演示
使用 CPP 實現(xiàn)基于ROS2的消息訂閱示例
- CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(ros2_pub_recv_poc)
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(std_msgs REQUIRED)
add_executable(ping_publisher src/ping_publisher.cpp)
ament_target_dependencies(ping_publisher rclcpp std_msgs)
add_executable(ping_subscriber src/ping_subscriber.cpp)
ament_target_dependencies(ping_subscriber rclcpp std_msgs)
install(TARGETS
ping_publisher
ping_subscriber
DESTINATION lib/${PROJECT_NAME})
ament_package()
- 生產(chǎn)者
#include <chrono>
#include <memory>
#include <iomanip>
#include <sstream>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class PingPublisher : public rclcpp::Node {
public:
PingPublisher() : Node("ping_publisher") {
publisher_ = this->create_publisher<std_msgs::msg::String>("ping", 10);
timer_ = this->create_wall_timer(
1000ms,
[this]() {
auto message = std_msgs::msg::String();
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()) % 1000;
std::stringstream ss;
ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
ss << '.' << std::setfill('0') << std::setw(3) << ms.count();
message.data = ss.str();
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
);
}
private:
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
};
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<PingPublisher>());
rclcpp::shutdown();
return 0;
}
- 訂閱者
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class PingSubscriber : public rclcpp::Node {
public:
PingSubscriber() : Node("ping_subscriber") {
subscription_ = this->create_subscription<std_msgs::msg::String>(
"ping",
10,
[this](const std_msgs::msg::String::SharedPtr msg) {
RCLCPP_INFO(this->get_logger(), "Received: '%s'", msg->data.c_str());
}
);
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<PingSubscriber>());
rclcpp::shutdown();
return 0;
}
還支持通過命令行方式來確認
# 打印 node 列表
ros2 node list
# 打印 topic 列表
ros2 topic list
# 命令行監(jiān)聽 /ping topic
ros2 topic echo /ping
#
0x08 后續(xù)學習規(guī)劃
第一階段:夯實 ROS2 通信機制
你已完成 Topic(話題),接下來補全另外兩種通信范式:
ROS2 三種通信方式
├── Topic(發(fā)布/訂閱) — 已掌握,適合持續(xù)數(shù)據(jù)流(傳感器、狀態(tài))
├── Service(服務) — 請求/響應模式,適合一次性指令(開關燈、查詢狀態(tài))
└── Action(動作) — 長時任務 + 進度反饋(導航到目標點、機械臂運動)
Action 是三者中最復雜、也最貼近真實機器人任務的,一定要掌握。
第二階段:坐標變換 TF2
這是 ROS2 中最核心的基礎設施之一,幾乎所有機器人功能都依賴它:
管理機器人各坐標系的空間關系(base_link → camera_link → map)
學會發(fā)布和查詢 TF 變換
理解靜態(tài) TF 和動態(tài) TF 的區(qū)別
沒有 TF2 的概念,后續(xù)做導航、感知都會寸步難行。
第三階段:仿真環(huán)境 Gazebo
在真實機器人上調(diào)試代價高、風險大,先在仿真里驗證:
搭建一個帶傳感器(激光雷達 / 攝像頭)的差速輪機器人
學會用 URDF / SDF 描述機器人模型
ROS2 + Gazebo Harmonic 的橋接(ros_gz_bridge)
第四階段:導航棧 Nav2
ROS2 導航的標準答案,也是檢驗前三階段是否掌握的綜合練習:
激光雷達數(shù)據(jù)
↓
SLAM 建圖(slam_toolbox)
↓
Nav2 路徑規(guī)劃 + 避障
↓
/cmd_vel 速度指令 → 機器人底盤
第五階段:機械臂 MoveIt2(按需)
如果方向是操作臂/協(xié)作機器人:
運動學求解、碰撞檢測、軌跡規(guī)劃
與 ros2_control 對接真實硬件