惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
WordPress大学
WordPress大学
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 叶小钗
博客园 - 聂微东
阮一峰的网络日志
阮一峰的网络日志
腾讯CDC
博客园 - 三生石上(FineUI控件)
V
V2EX
有赞技术团队
有赞技术团队
V
Visual Studio Blog
小众软件
小众软件
Jina AI
Jina AI
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - Franky
量子位
T
Tailwind CSS Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Palo Alto Networks Blog
Cisco Talos Blog
Cisco Talos Blog
I
Intezer
Project Zero
Project Zero
A
Arctic Wolf
P
Privacy International News Feed
V
Vulnerabilities – Threatpost
L
Lohrmann on Cybersecurity
S
Securelist
C
Cybersecurity and Infrastructure Security Agency CISA
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Tor Project blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
Security @ Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
TaoSecurity Blog
TaoSecurity Blog
L
LINUX DO - 热门话题
G
GRAHAM CLULEY
Help Net Security
Help Net Security
N
News | PayPal Newsroom
W
WeLiveSecurity
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
C
Check Point Blog

博客园 - osbreak

QML::qml与c++数据交互 k8s:: Service 管理 deployment postgresql 索引 postgresql 基本类型 postgresql 基础运维 PLSQL 触发器 PLSQL 程序包 PLSQL 执行块 PLSQL 基础数据类型 PLSQL oracle安装部署 mysql事务与隔离 mysql慢查询分析 mysql建增删改查 mysql常用总结 (9)libevent 常用设置 (8)libevent 构建libevent http服务,支持文件下载 (7)libevent filter(过滤器) (6)libevent定时器 (5)libevent evbuffer
ros2::tf2
osbreak · 2026-02-09 · via 博客园 - osbreak
1.0 turtlesim 包
turtlesim 是 ROS2 官方提供的一个经典教学包,它实现了一个简单的二维乌龟(Turtle)仿真器
1.1 turtlesim::控制乌龟移动
# 显示一个包含一只初始乌龟的窗口
rosrun turtlesim turtlesim_node

# ros2 topic pub <topic_name> <msg_type> <arg1> <arg2> ...
# 发布消息次数(一次): -1/--once
# 消息类型:geometry_msgs/msg/Twist
# 消息内容(YAML格式):
#   -- x轴以 1.0 m/s 的线速度前进,y轴和z轴线速度为0,
#   -- x, y, z 轴的角速度都为0。
ros2 topic pub -1 /turtle1/cmd_vel geometry_msgs/msg/Twist '{linear: {x: 1.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}'

# 以n的频率发布消息:-r -次数
ros2 topic pub -r 次数 /turtle1/cmd_vel geometry_msgs/msg/Twist '{linear: {x: 1.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}'
1.2 turtlesim::检查可用的topic和服务Server
# 显示一个包含一只初始乌龟的窗口
rosrun turtlesim turtlesim_node

# 查看所有服务
ros2 service list
------------------------------
/clear
/kill
/reset
/spawn
/turtle1/set_pen
/turtle1/teleport_absolute
/turtle1/teleport_relative
/turtlesim/describe_parameters
/turtlesim/get_parameter_types
/turtlesim/get_parameters
/turtlesim/list_parameters
/turtlesim/set_parameters
/turtlesim/set_parameters_atomically
------------------------------

# 查看特定节点提供的服务
ros2 node info /turtlesim
-------------------------------
/turtlesim
  Subscribers:
    /parameter_events: rcl_interfaces/msg/ParameterEvent
    /turtle1/cmd_vel: geometry_msgs/msg/Twist
  Publishers:
    /parameter_events: rcl_interfaces/msg/ParameterEvent
    /rosout: rcl_interfaces/msg/Log
    /turtle1/color_sensor: turtlesim/msg/Color
    /turtle1/pose: turtlesim/msg/Pose
  Service Servers:
    /clear: std_srvs/srv/Empty
    /kill: turtlesim/srv/Kill
    /reset: std_srvs/srv/Empty
    /spawn: turtlesim/srv/Spawn
    /turtle1/set_pen: turtlesim/srv/SetPen
    /turtle1/teleport_absolute: turtlesim/srv/TeleportAbsolute
    /turtle1/teleport_relative: turtlesim/srv/TeleportRelative
    /turtlesim/describe_parameters: rcl_interfaces/srv/DescribeParameters
    /turtlesim/get_parameter_types: rcl_interfaces/srv/GetParameterTypes
    /turtlesim/get_parameters: rcl_interfaces/srv/GetParameters
    /turtlesim/list_parameters: rcl_interfaces/srv/ListParameters
    /turtlesim/set_parameters: rcl_interfaces/srv/SetParameters
    /turtlesim/set_parameters_atomically: rcl_interfaces/srv/SetParametersAtomically
  Service Clients:

  Action Servers:
    /turtle1/rotate_absolute: turtlesim/action/RotateAbsolute
  Action Clients:
-------------------------------
1.3 检查topic消息类型和字段
ros2 topic type <topic_name>
-------------------------------
ros2 topic type /turtle1/cmd_vel
geometry_msgs/msg/Twist
-------------------------------

ros2 topic info <topic_name>
-------------------------------
ros2 topic  info /turtle1/cmd_vel
Type: geometry_msgs/msg/Twist
Publisher count: 0
Subscription count: 1
-------------------------------
1.4 检查Server接口及字段
查看某个接口服务
ros2 service type /spawn
-------------------------------
turtlesim/srv/Spawn
-------------------------------

查看服务接口的详细定义:
ros2 interface show turtlesim/srv/Spawn
-------------------------------
float32 x
float32 y
float32 theta
string name
---
string name
-------------------------------
1.5 调用接口,生成第二只小乌龟
ros2 service call /spawn turtlesim/srv/Spawn "{name: 'turtle2', x: 2.0, y: 2.0, theta: 0.0}"
1.6 小乌龟坐标系
ros2 run learning_tf2_cpp turtle_tf2_broadcaster --ros-args -p turtlename:=turtle1
ros2 run learning_tf2_cpp turtle_tf2_broadcaster --ros-args -p turtlename:=turtle2

1. turtle1 和 turtle2 是 turtlesim 仿真器中的两个完全独立的、可移动的虚拟乌龟。
2. 共享环境: 它们都在同一个二维平面(蓝色窗口)内活动。
3. 独立的坐标系: 当你运行两个 turtle_tf2_broadcaster 节点时:
      -- 创建一个名为 turtle1_frame 的坐标系
      -- 创建一个名为 turtle2_frame 的坐标系
      -- turtle1_frame 和 turtle2_frame 都以 world 作为它们的父坐标系。world 可以理解为仿真世界的绝对参考系。
      -- 相对位置查询: 由于 turtle1_frame 和 turtle2_frame 都连接到了同一个 world 坐标系下,TF2 系统就能够计算出 turtle2_frame 相对于 turtle1_frame 的位置和方向。这就是 turtle_tf2_listener 能够实现追踪的关键。

4. 独立的控制: turtlesim_node 为每只乌龟提供独立的话题接口:
      -- 可以通过向 /turtle1/cmd_vel 发布消息来控制 turtle1
      -- 可以通过向 /turtle2/cmd_vel 发布消息来控制 turtle2
5. 独立的位姿: turtlesim_node 也为每只乌龟发布独立的位姿信息:
      -- turtle1 的位姿通过 /turtle1/pose 话题发布
      -- turtle2 的位姿通过 /turtle2/pose 话题发布
1.7 turtlesim、turtle_tf2_broadcaster、listener间关系
1. turtlesim 负责模拟 turtle1 和 turtle2 的物理行为,并提供它们的位姿数据 (/turtle1/pose, /turtle2/pose)。

2. turtle_tf2_broadcaster 节点分别监听这些位姿数据,并将每只乌龟的位置和方向转换为一个独立的 TF 坐标系 (turtle1_frame, turtle2_frame),并发布到 TF 树中。

3. turtle_tf2_listener 节点则利用 TF2 库的强大功能,查询 turtle2_frame 相对于 turtle1_frame 的变换(即 turtle2 在 turtle1 坐标系下的位置 (x, y))。根据这个相对位置,listener 节点计算出 turtle2 需要的运动指令 (Twist),并发布到 /turtle2/cmd_vel 话题,从而驱动 turtle2 去追赶 turtle1。


总结核心要点: 
turtle1 和 turtle2 本身是独立的。
但通过 TF2 框架,我们为它们各自创建了坐标系 (turtle1_frame, turtle2_frame),并将这些坐标系链接到一个公共的参考系 (world) 下。
这就使得我们可以方便地计算它们之间的相对关系,从而实现像“追踪”这样的高级功能。
1.8 小乌龟跟随
ros2 pkg create learning_tf2_cpp --build-type ament_cmake --dependencies rclcpp tf2 tf2_ros geometry_msgs turtlesim 
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>learning_tf2_cpp</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="bk@todo.todo">bk</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend>
  <depend>tf2</depend>
  <depend>tf2_ros</depend>
  <depend>geometry_msgs</depend>
  <depend>turtlesim</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>
cmake_minimum_required(VERSION 3.8)
project(learning_tf2_cpp)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tf2 REQUIRED)
find_package(tf2_ros REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(turtlesim REQUIRED)


# --- 添加可执行文件定义 ---
add_executable(turtle_tf2_broadcaster src/turtle_tf2_broadcaster.cpp)
ament_target_dependencies(turtle_tf2_broadcaster
  rclcpp tf2 tf2_ros geometry_msgs turtlesim)

add_executable(turtle_tf2_listener src/turtle_tf2_listener.cpp)
ament_target_dependencies(turtle_tf2_listener
  rclcpp tf2 tf2_ros geometry_msgs turtlesim)

# Install executables
install(TARGETS
  turtle_tf2_broadcaster
  turtle_tf2_listener
  DESTINATION lib/${PROJECT_NAME}
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  set(ament_cmake_copyright_FOUND TRUE)
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()
// === 头文件包含 ===
// 用于处理 TF 变换相关的消息类型
#include <geometry_msgs/msg/transform_stamped.hpp>
// ROS2 C++ 客户端库的核心头文件
#include <rclcpp/rclcpp.hpp>
// TF2 库,用于处理旋转(四元数)
#include <tf2/LinearMath/Quaternion.h>
// TF2 ROS 包装库,用于发布坐标变换
#include <tf2_ros/transform_broadcaster.h>
// 用于订阅 Turtlesim 的位姿消息类型
#include "turtlesim/msg/pose.hpp"


// === 定义广播器类 ===
// 继承自 rclcpp::Node,这是一个 ROS2 节点
class FramePublisher : public rclcpp::Node
{
public:
  // 构造函数
  FramePublisher()
  : Node("turtle_tf2_broadcaster") // 调用父类构造函数,设置节点名称为 "turtle_tf2_broadcaster"
  {
    // --- 1. 声明并获取参数 ---
    // 声明一个名为 "turtlename" 的参数,类型为字符串,如果用户没有指定,则默认值为 "turtle1"
    this->declare_parameter<std::string>("turtlename", "turtle1");

    // 从参数服务器获取 "turtlename" 参数的实际值
    // 这个值由运行时的命令行参数 --ros-args -p turtlename:=... 指定
    turtlename_ = this->get_parameter("turtlename").as_string();

    // --- 2. 创建订阅者 ---
    // 创建一个 Subscription,用于订阅乌龟的位姿信息
    // 话题名称是动态构建的:turtlename_ + "/pose" (例如 "turtle1/pose")
    // 消息队列大小为 1
    // 当收到消息时,会调用 pose_callback 回调函数,并将消息作为参数传入
    using std::placeholders::_1; // 用于 std::bind
    subscription_ = this->create_subscription<turtlesim::msg::Pose>(
      turtlename_ + "/pose", 1, // 动态构建话题名称
      std::bind(&FramePublisher::pose_callback, this, _1) // 绑定回调函数
    );

    // --- 3. 创建 TransformBroadcaster ---
    // 创建一个 TF2 TransformBroadcaster
    // 它用于将坐标变换信息发布到 ROS2 的 /tf 话题上
    // 需要传入当前节点的指针 (this),以便它能正确地发布消息
    tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(this);
  }

private:
  // --- 位姿回调函数 ---
  // 这个函数会在每次从 turtlename_/pose 话题收到新的位姿消息时被调用
  void pose_callback(const turtlesim::msg::Pose::SharedPtr msg)
  {
    // --- 4. 构建 TransformStamped 消息 ---
    // 创建一个 geometry_msgs::msg::TransformStamped 消息
    // 这个消息将包含一个坐标系相对于另一个坐标系的完整变换信息(位置和方向)
    geometry_msgs::msg::TransformStamped t;

    // 设置消息头
    // stamp: 设置时间戳,表示这个变换数据的生成时间
    t.header.stamp = this->get_clock()->now();
    // frame_id: 设置父坐标系的名称
    t.header.frame_id = "world"; // 将 "world" 设为父坐标系,这是一个通用的参考系

    // 设置子坐标系的名称
    // child_frame_id: 设置子坐标系的名称,通常是动态构建的 (例如 "turtle1_frame")
    t.child_frame_id = turtlename_ + "_frame";

    // --- 5. 填充位置 (translation) 信息 ---
    // 从 Turtlesim 的 Pose 消息中获取 x, y 坐标
    // 并填充到 TransformStamped 消息的 translation 部分
    t.transform.translation.x = msg->x; // x 轴位置
    t.transform.translation.y = msg->y; // y 轴位置
    t.transform.translation.z = 0.0;   // z 轴位置,在 2D 平面仿真中始终为 0

    // --- 6. 填充方向 (rotation) 信息 ---
    // Turtlesim 的 Pose 消息中的 theta 是一个标量(弧度),表示朝向
    // TF2 使用四元数 (quaternion) 来表示方向,所以我们需要转换
    // 创建一个 tf2::Quaternion 对象
    tf2::Quaternion q;
    // 使用 setRPY 函数将欧拉角 (Roll, Pitch, Yaw) 转换为四元数
    // 在 2D 平面中,只有 Yaw (偏航角) 有意义,所以 R 和 P 设为 0
    // msg->theta 就是我们需要的 Yaw 角
    q.setRPY(0, 0, msg->theta);

    // 将计算出的四元数各分量赋值给 TransformStamped 消息的 rotation 部分
    t.transform.rotation.x = q.x();
    t.transform.rotation.y = q.y();
    t.transform.rotation.z = q.z();
    t.transform.rotation.w = q.w();

    // --- 7. 发布变换 ---
    // 使用之前创建的 TransformBroadcaster 将构建好的变换消息发布出去
    // 这个消息会被发送到 /tf 话题上,供其他节点(如 turtle_tf2_listener)使用
    tf_broadcaster_->sendTransform(t);
  }

  // --- 成员变量声明 ---
  // 存储订阅者,用于监听乌龟的位姿
  rclcpp::Subscription<turtlesim::msg::Pose>::SharedPtr subscription_;
  // 存储 TransformBroadcaster,用于发布坐标系变换
  std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
  // 存储乌龟的名称(例如 "turtle1" 或 "turtle2"),用于构建话题名和坐标系名
  std::string turtlename_;
};

// === 主函数 ===
int main(int argc, char * argv[])
{
  // 初始化 ROS2 客户端库
  rclcpp::init(argc, argv);

  // 创建 FramePublisher 节点的实例并将其传递给 rclcpp::spin
  // spin 函数会进入事件循环,处理订阅的消息(触发 pose_callback)、定时器等
  rclcpp::spin(std::make_shared<FramePublisher>());

  // 关闭 ROS2 客户端库
  rclcpp::shutdown();
  return 0;
}
// === 头文件包含 ===
// 用于处理 TF 变换相关的消息类型
#include <geometry_msgs/msg/transform_stamped.hpp>
// 用于发送速度指令的消息类型
#include <geometry_msgs/msg/twist.hpp>
// ROS2 C++ 客户端库的核心头文件
#include <rclcpp/rclcpp.hpp>
// TF2 库,用于处理旋转(四元数)
#include <tf2/LinearMath/Quaternion.h>
// TF2 ROS 包装库,用于缓存和监听变换
#include <tf2_ros/buffer.h>
#include <tf2_ros/create_timer_ros.h> // 用于 TF2 内部计时器
#include <tf2_ros/transform_listener.h> // TF2 监听器类
// 用于调用 Turtlesim 服务(本例中未实际使用,但可能在其他变体中用到)
#include "turtlesim/srv/spawn.hpp"


// === 定义监听器类 ===
// 继承自 rclcpp::Node,这是一个 ROS2 节点
class TurtleTF2Listener : public rclcpp::Node
{
public:
  // 构造函数
  TurtleTF2Listener()
  : Node("turtle_tf2_listener") // 调用父类构造函数,设置节点名称为 "turtle_tf2_listener"
  {
    // --- 1. 初始化 TF2 Buffer 和 Listener ---
    // 创建一个 TF2 Buffer,用于缓存从 /tf 话题接收到的坐标变换数据
    // get_clock() 提供时钟,以便 Buffer 能够处理带时间戳的变换
    tf_buffer_ = std::make_unique<tf2_ros::Buffer>(this->get_clock());

    // 创建一个 TF2 的计时器接口,这是 TF2 内部用于异步操作(如超时)所需的
    // 它需要访问节点的基础接口和定时器接口
    auto timer_interface = std::make_shared<tf2_ros::CreateTimerROS>(
      this->get_node_base_interface(),    // 节点基础接口(用于节点名称等)
      this->get_node_timers_interface()); // 节点定时器接口(用于创建内部定时器)

    // 将创建好的计时器接口设置给 Buffer
    tf_buffer_->setCreateTimerInterface(timer_interface);

    // 创建一个 TF2 TransformListener
    // 它会自动订阅 /tf 和 /tf_static 话题,并将收到的变换数据存储到 tf_buffer_ 中
    tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);


    // --- 2. 创建速度指令发布者 ---
    // 创建一个 Publisher,用于向 Turtlesim 发送速度指令
    // 话题名称是 "/turtle2/cmd_vel",队列大小为 1
    cmd_vel_publisher_ = this->create_publisher<geometry_msgs::msg::Twist>(
      "/turtle2/cmd_vel", 1);


    // --- 3. 创建定时器 ---
    // 创建一个 Wall Timer(基于系统时间的定时器)
    // 每隔 50 毫秒(即频率约为 20 Hz)执行一次 lookup_loop 函数
    using namespace std::chrono_literals; // 方便使用 ms, s 等时间字面量
    timer_ = this->create_wall_timer(
      50ms, // 定时器间隔
      std::bind(&TurtleTF2Listener::lookup_loop, this) // 绑定回调函数到当前对象
    );
  }

private:
  // --- 核心循环函数 ---
  // 这个函数会被定时器定期调用,执行查询变换和控制逻辑
  void lookup_loop()
  {
    // 定义源坐标系和目标坐标系
    // 注意:这里的名称必须与 TF 树中存在的坐标系名称完全一致
    const std::string target_frame = "turtle2_frame"; // 目标坐标系,我们想知道它相对于源坐标系的位置
    const std::string source_frame = "turtle1_frame"; // 源坐标系,作为参考坐标系

    try {
      // --- 4. 查询坐标变换 ---
      // 使用 tf_buffer_ 查询从 source_frame 到 target_frame 的变换
      // lookupTransform(target_frame, source_frame, time_point)
      // tf2::TimePointZero 表示查询最新可用的变换(不关心具体时间戳)
      geometry_msgs::msg::TransformStamped transformStamped = tf_buffer_->lookupTransform(
        target_frame, source_frame, tf2::TimePointZero);

      // --- 5. 提取位置信息 ---
      // 从查询到的变换中提取目标坐标系 (turtle2_frame) 相对于源坐标系 (turtle1_frame) 的位置
      // translation.x 和 translation.y 就是 turtle2 在 turtle1 坐标系下的 x, y 坐标
      double x = transformStamped.transform.translation.x;
      double y = transformStamped.transform.translation.y;

      // --- 6. 控制逻辑:计算速度指令 ---
      // 创建一个 Twist 消息,用于存储线速度和角速度
      geometry_msgs::msg::Twist twist;

      // 计算角速度 (angular.z),使 turtle2 朝向 turtle1
      // atan2(y, x) 计算从 turtle2 指向 turtle1 的角度(弧度)
      // 乘以 4.0 是一个增益系数,用于加快转向速度
      twist.angular.z = 4.0 * atan2(y, x);

      // 计算线速度 (linear.x),使 turtle2 向 turtle1 移动
      // sqrt(x^2 + y^2) 计算 turtle2 到 turtle1 的直线距离
      // 乘以 0.5 是一个增益系数,用于设定线速度与距离的比例
      twist.linear.x = 0.5 * sqrt(pow(x, 2) + pow(y, 2));

      // --- 7. 发布速度指令 ---
      // 将计算好的速度指令发布到 "/turtle2/cmd_vel" 话题
      // Turtlesim 节点会订阅这个话题,并驱动 turtle2 按照指令移动
      cmd_vel_publisher_->publish(twist);

    } catch (const tf2::TransformException & ex) { // 捕获 TF2 变换异常
      // 如果查询失败(例如,指定的坐标系不存在,或缓冲区中没有足够新的数据)
      // 打印错误信息到日志,便于调试
      RCLCPP_INFO(
        this->get_logger(), // 获取当前节点的日志记录器
        "Could not transform %s to %s: %s", // 错误信息模板
        target_frame.c_str(), source_frame.c_str(), ex.what()); // 填充模板变量
    }
  }

  // --- 成员变量声明 ---
  // 存储 TF2 缓冲区,用于缓存变换数据
  std::unique_ptr<tf2_ros::Buffer> tf_buffer_;
  // 存储 TF2 监听器,自动接收并更新变换数据到缓冲区
  std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
  // 存储速度指令发布者,用于向 turtle2 发送运动指令
  rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_vel_publisher_;
  // 存储定时器,用于周期性执行 lookup_loop
  rclcpp::TimerBase::SharedPtr timer_;
};

// === 主函数 ===
int main(int argc, char * argv[])
{
  // 初始化 ROS2 客户端库
  rclcpp::init(argc, argv);

  // 创建 TurtleTF2Listener 节点的实例并将其传递给 rclcpp::spin
  // spin 函数会进入事件循环,处理订阅的消息、定时器回调等
  rclcpp::spin(std::make_shared<TurtleTF2Listener>());

  // 关闭 ROS2 客户端库
  rclcpp::shutdown();
  return 0;
}
1.9 小乌龟跟随运行验证
【终端 1】启动 Turtlesim 仿真器:
ros2 run turtlesim turtlesim_node

【终端 2】生成第二只乌龟 turtle2:
ros2 service call /spawn turtlesim/srv/Spawn "{name: 'turtle2', x: 2.0, y: 2.0, theta: 0.0}"

【终端 3】启动 turtle1 的 TF 广播器:
ros2 run learning_tf2_cpp turtle_tf2_broadcaster --ros-args -p turtlename:=turtle1

【终端 4】启动 turtle2 的 TF 广播器:
ros2 run learning_tf2_cpp turtle_tf2_broadcaster --ros-args -p turtlename:=turtle2

【终端 5】最后启动 TF 监听器和控制器:
ros2 run learning_tf2_cpp turtle_tf2_listener