ROS2-13章:机器人仿真环境介绍Turtlesim示例

目录

++[目录... 1](#目录... 1)++

++[一、Turtlesim简介... 2](#一、Turtlesim简介... 2)++

++[1.1核心可执行节点和主要接口... 2](#1.1核心可执行节点和主要接口... 2)++

++[1.2说明与局限... 2](#1.2说明与局限... 2)++

++[二、创建新的工作空间... 3](#二、创建新的工作空间... 3)++

++[2.1 创建功能包turtle_exercsie. 3](#2.1 创建功能包turtle_exercsie. 3)++

++[2.2 仿真功能的示例-话题控制... 3](#2.2 仿真功能的示例-话题控制... 3)++

++[2.2.1 话题控制-通过话题控制小海龟实现螺旋式运动轨迹... 3](#2.2.1 话题控制-通过话题控制小海龟实现螺旋式运动轨迹... 3)++

++[2.3 仿真功能的示例-服务调用... 9](#2.3 仿真功能的示例-服务调用... 9)++

++[2.3.1 服务调用方式控制多个小海龟绘制彩色奥运五环... 9](#2.3.1 服务调用方式控制多个小海龟绘制彩色奥运五环... 9)++

++[2.4 仿真功能的示例-动作反馈... 14](#2.4 仿真功能的示例-动作反馈... 14)++

++[2.4.1 创建动作接口功能包-小海龟通过指令话多边形... 14](#2.4.1 创建动作接口功能包-小海龟通过指令话多边形... 14)++

++[第三章 实际开发中的选型指南... 18](#第三章 实际开发中的选型指南... 18)++

++[3.1、 话题(Topic)与服务(Service)的优缺点对比... 18](#3.1、 话题(Topic)与服务(Service)的优缺点对比... 18)++

++[3.2、 结合你的案例来分析... 18](#3.2、 结合你的案例来分析... 18)++

++[3.3、 实战指南:开发时如何选择?... 19](#3.3、 实战指南:开发时如何选择?... 19)++

一、Turtlesim简介

**turtlesim(小乌龟模拟器)**​ 是 ROS2 官方提供的轻量级二维教学仿真工具,并非真实机器人或完整物理仿真器,其设计目的是帮助初学者直观理解 ROS2 的核心概念------节点(Node)、话题(Topic)、服务(Service)、动作(Action)及参数(Parameter)。

turtlesim 启动后会弹出一个蓝色背景的 2D 窗口,其中有一只可程序控制运动的小乌龟(turtle),常用于演示 ROS2 通信机制与命令行工具的使用。

1.1核心可执行节点和主要接口

  • turtlesim_node:主仿真节点,负责渲染画布、维护乌龟位姿,订阅速度指令并发布位姿信息。
  • turtle_teleop_key:键盘控制节点,将方向键映射为 geometry_msgs/msg/Twist消息发布到 /turtle1/cmd_vel。
  • draw_square/ mimic:示例程序,分别演示自动画正方形和运动跟随。

主要通信接口

  • 话题 Topic
    • /turtle1/cmd_vel--- 订阅 geometry_msgs/Twist,接收线速度与角速度指令控制乌龟运动。
    • /turtle1/pose--- 发布 turtlesim/msg/Pose,广播乌龟当前坐标 (x, y)、朝向 θ及速度。
  • 服务 Service
    • /spawn--- 生成新乌龟(指定坐标、朝向、名称)。
    • /kill--- 删除指定乌龟。
    • /reset--- 重置画布与乌龟至初始状态。
    • /turtle1/set_pen--- 设置乌龟画笔颜色、线宽及是否落笔。
    • /turtle1/teleport_absolute/ teleport_relative--- 绝对/相对瞬移。
  • 动作 Action
    • /turtle1/rotate_absolute--- 控制乌龟旋转到指定朝向,带反馈与可取消。
  • 参数 Parameter
    • /turtlesim节点提供 background_r、background_g、background_b参数,可在运行时动态修改画布背景色。

启动方式示例

终端1:启动仿真器

ros2 run turtlesim turtlesim_node

终端2:启动键盘控制(鼠标聚焦此终端后用↑↓←→控制)

ros2 run turtlesim turtle_teleop_key

1.2说明与局限

turtlesim 不具备物理仿真能力(无重力、碰撞、摩擦力等),不能替代 Gazebo 等物理仿真器。它仅用于 ROS2 基础概念学习与通信机制验证,是进入真实机器人或 Gazebo 仿真前的"Hello World"级入门工具。

二、创建新的工作空间

2.1 创建功能包turtle_exercsie

在 ROS 2 中创建一个新的工作空间ros2_ws1,并创建一个名称是turtle_exercise的功能包,下面示例使用Python功能包进行。

首先在终端执行如下命令:创建功能包和代码存放文件夹src;进入src文件里,创建我的小海龟练习功能包文件turtle_exercise;

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| root@LHAYR:~# ls ros2_ws ros_ws root@LHAYR:~# mkdir -p ~/ros2_ws1/src root@LHAYR:~# cd ros2_ws1/src root@LHAYR:~/ros2_ws1/src# ros2 pkg create --build-type ament_python --license MIT turtle_exercise going to create a new package package name: turtle_exercise 。。。。。。。。 root@LHAYR:~/ros2_ws1/src# |

2.2 仿真功能的示例-话题控制

话题(Topic)是一种基于发布者--订阅者(Publisher--Subscriber)模型的通信机制,允许节点通过发布和订阅消息实现松耦合的数据交换。在 ROS2 中,话题是节点间传输数据流的主要方式,特别适用于连续、高频、实时性要求较高的数据(如传感器数据、控制指令等)。

在 Turtlesim 仿真环境中,可以通过话题实时控制小海龟的运动状态,例如发布速度指令驱动其移动,或订阅位姿话题实时获取其在画布中的坐标与朝向。借助话题机制,系统能够以较低延迟完成对小海龟运动的实时控制与状态监控,因此话题也被认为是各类 ROS2 仿真与控制任务中最常用、最核心的通信工具之一。

本案例通过 Turtlesim 提供的速度控制话题 /turtle1/cmd_vel,控制小海龟沿螺旋线轨迹运动,效果如图 X-X 所示。其实现原理为:在保持角速度恒定的前提下,使线速度随时间线性增大,从而使小海龟在旋转的同时不断向外"扩散",形成螺旋线轨迹。具体实现步骤如下:

URDF 文件主要包括加载机器人模型和发布机器人关节状态两个过程。机器人模型的发布需要使用机器人状态发布器(Robot State Publisher)。机器人状态发布器将机器人的状态发布到tf2,使得ROS2能够了解机器人内部各连杆和关节的坐标系,从而确定机器人的状态。

2.2.1 话题控制-通过话题控制小海龟实现螺旋式运动轨迹

在turtle_exercise功能包里,创建一个节点代draw_spiral.py,代码写入内容如下:

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| * * 创建draw_spiral.py root@LHAYR:~/ros2_ws1# cd ~/ros2_ws1/src/turtle_exercise/turtle_exercise root@LHAYR:~/ros2_ws1/src/turtle_exercise/turtle_exercise# touch draw_spiral.py root@LHAYR:~/ros2_ws1/src/turtle_exercise/turtle_exercise# chmod +x draw_spiral.py root@LHAYR:~/ros2_ws1/src/turtle_exercise/turtle_exercise# ls init.py draw_spiral.py root@LHAYR:~/ros2_ws1/src #code . |
| * * vs code 打开draw_spiral.py,代码写入内容如下 |
| #!/usr/bin/env python3 import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist class SpeedPublisher(Node): def init(self): super().init('draw_spiral') # 声明参数,默认控制名为 'turtle1' 的小海龟 self.turtle_name = self.declare_parameter('turtle_name', 'turtle1').get_parameter_value().string_value # 创建发布者,向对应小海龟的 cmd_vel 话题发布速度指令 self.publisher = self.create_publisher(Twist, f'{self.turtle_name}/cmd_vel', 10) # 创建定时器,每 0.1 秒触发一次回调函数 self.timer = self.create_timer(0.1, self.on_timer) # 初始化 Twist 消息 self.cmd = Twist() self.cmd.linear.x = 0.0 # 初始线速度为 0 self.cmd.angular.z = 3.0 # 初始角速度为 3.0 def on_timer(self): # 每次定时器触发,线速度增加 0.03,使运动半径逐渐变大 self.cmd.linear.x += 0.03 self.publisher.publish(self.cmd) def main(args=None): rclpy.init(args=args) speed_publisher = SpeedPublisher() try: rclpy.spin(speed_publisher) except KeyboardInterrupt: pass finally: speed_publisher.destroy_node() rclpy.shutdown() if name == 'main': main() |
| * * 修改setup.pyw文件,将螺旋运动控制节点添加到entry_points中 |
| entry_points={ 'console_scripts': # 格式为:'可执行文件名 = 功能包名.文件名:主函数名' 'draw_spiral = turtle_exercise.draw_spiral:main', , |
| |

在turtle_exercise功能包里,先创建一个launch文件夹,再创建一个spiral_launch.py的launch文件,代码写入内容如下: 用launch文件直接启动小海龟turtlesim_node和控制节点draw_spiral.py

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| * 先创建文件夹,在创建文件 |
| root@LHAYR:~/ros2_ws1/src/turtle_exercise/turtle_exercise# cd .. root@LHAYR:~/ros2_ws1/src/turtle_exercise# mkdir -p launch root@LHAYR:~/ros2_ws1/src/turtle_exercise# cd launch root@LHAYR:~/ros2_ws1/src/turtle_exercise/launch# touch spiral_launch.py root@LHAYR:~/ros2_ws1/src/turtle_exercise/launch# ls spiral_launch.py root@LHAYR:~/ros2_ws1/src/turtle_exercise/launch# |
| * 在vs code编译器中,打开spiral_launch.py文件,输入下面代码, |
| from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription( Node( package='turtlesim', executable='turtlesim_node', name='sim' ), Node( package='turtle_exercise', executable='draw_spiral', name='drawspiral', parameters=\[ {"turtlename": "turtle1"} ) ]) |
| * 修改setup.py文件,把launch文件路径添加到data_files变量中 |
| 顶部添加: |-----------------------------------------------------------------------------| | import os from glob import glob from setuptools import find_packages, setup | 在data_files添加('share/' + package_name + '/launch', glob('launch/*launch.py')) |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | data_files= ('share/ament_index/resource_index/packages', \['resource/' + package_name), ('share/' + package_name, 'package.xml'), ('share/' + package_name + '/launch', glob('launch/*launch.py')) ], | |

编译,加载,运行

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| root@LHAYR:~/ros2_ws1# colcon build Starting >>> turtle_exercise Finished <<< turtle_exercise 0.58s Summary: 1 package finished 0.66s root@LHAYR:~/ros2_ws1# source install/setup.bash root@LHAYR:~/ros2_ws1# ros2 launch turtle_exercise spiral_launch.py INFO launch: All log files can be found below /root/.ros/log/2026-07-10-17-05-33-959101-LHAYR-3451 INFO launch: Default logging verbosity is set to INFO INFO turtlesim_node-1: process started with pid 3454 INFO draw_spiral-2: process started with pid 3455 turtlesim_node-1 INFO 1783674334.278782642 sim: Starting turtlesim with node name /sim turtlesim_node-1 INFO 1783674334.290728612 sim: Spawning turtle turtle1 at x=5.544445, y=5.544445, theta=0.000000 turtlesim_node-1 WARN 1783674390.233078662 sim: Oh no! I hit the wall! (Clamping from x=6.048432, y=-0.020131) turtlesim_node-1 WARN 1783674390.249716780 sim: Oh no! I hit the wall! (Clamping from x=5.781527, y=-0.022364) |
| 运行结果如下: |
| |

升级版本,小海龟路径是五角星,修改draw_spiral.py代码

python 复制代码
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from turtlesim.srv import SetPen, TeleportAbsolute
import math

class StarDrawer(Node):
    def __init__(self):
        super().__init__('star_drawer')
        
        # 1. 创建发布者
        self.cmd_pub = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
        
        # 2. 定义状态机变量
        # 你的逻辑:4 -> 8 -> 8 -> 8 -> 8 -> 4 (共6段动作)
        self.segment_lengths = [4.0, 8.0, 8.0, 8.0, 8.0, 4.0] 
        self.current_segment = 0
        
        # 物理参数
        self.linear_speed = 2.0   # 前进速度 m/s
        self.angular_speed = 1.5  # 转弯速度 rad/s (约86度/秒)
        self.turn_angle = 144.0 * (math.pi / 180.0) # 目标转角:144度
        
        # 计时器
        self.timer = self.create_timer(0.05, self.control_loop) # 20Hz 控制频率
        self.elapsed_time = 0.0
        
        # 初始化状态
        self.state = 'MOVING' # MOVING 或 TURNING
        self.reset_turtle()

    def reset_turtle(self):
        """强制将海龟重置到中心并朝向正右方"""
        client = self.create_client(TeleportAbsolute, '/turtle1/teleport_absolute')
        while not client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('等待重置服务...')
        
        req = TeleportAbsolute.Request()
        req.x = 5.544445  # 窗口中心 X
        req.y = 5.544445  # 窗口中心 Y
        req.theta = 0.0   # 【关键】强制朝向正右方 (0弧度)
        
        future = client.call_async(req)
        rclpy.spin_until_future_complete(self, future)
        self.get_logger().info("已重置位置和朝向!开始画星...")

    def control_loop(self):
        twist = Twist()
        dt = 0.05 # 定时器间隔
        
        if self.current_segment >= len(self.segment_lengths):
            # 画完了,停止
            twist.linear.x = 0.0
            twist.angular.z = 0.0
            self.cmd_pub.publish(twist)
            self.get_logger().info("五角星绘制完成!")
            self.destroy_timer(self.timer)
            return

        target_dist = self.segment_lengths[self.current_segment]

        if self.state == 'MOVING':
            # --- 直线运动阶段 ---
            if self.elapsed_time < (target_dist / self.linear_speed):
                twist.linear.x = self.linear_speed
                twist.angular.z = 0.0
            else:
                # 距离到了,准备转弯
                twist.linear.x = 0.0
                twist.angular.z = 0.0
                self.state = 'TURNING'
                self.elapsed_time = 0.0 # 重置计时器用于转弯
                
        elif self.state == 'TURNING':
            # --- 旋转阶段 ---
            turn_duration = self.turn_angle / self.angular_speed
            
            if self.elapsed_time < turn_duration:
                twist.linear.x = 0.0
                twist.angular.z = self.angular_speed # 向右转 (正值在TurtleSim中通常是逆时针,若方向反了改为负值)
                # 注意:TurtleSim中 angular.z > 0 是逆时针(左转),< 0 是顺时针(右转)
                # 五角星外角144度通常是向一个方向连续转。
                # 如果画出来是反的,把下面这行改成 -self.angular_speed
                twist.angular.z = -self.angular_speed 
            else:
                # 转弯结束,进入下一段直线
                twist.linear.x = 0.0
                twist.angular.z = 0.0
                self.state = 'MOVING'
                self.elapsed_time = 0.0
                self.current_segment += 1

        self.cmd_pub.publish(twist)
        self.elapsed_time += dt

def main(args=None):
    rclpy.init(args=args)
    node = StarDrawer()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

|--------|
| |
| 运行结果如下 |

2.3 仿真功能的示例-服务调用

服务是一种基于请求一响应模型的通信机制,在ROS2中使用服务可以调用预定义的功能来实现特定的任务。在Turtlesim仿真环境中,提供了多个不司的服务,如生成小海龟,重置环境,移除小海龟等。

在 Turtlesim 仿真环境中,/spawn 服务允许我们通过发送请求来动态生成新的小海龟,并精确指定其名称、位置和朝向。

🛠️ 核心原理:/spawn 服务

  • 服务类型:turtlesim/srv/Spawn
  • 请求参数:
    • x, y (float32): 小海龟生成的坐标位置。
    • theta (float32): 小海龟的初始朝向角度(弧度制)。
    • name (string): 小海龟的唯一标识名称。
  • 响应参数:
    • name (string): 返回生成成功的小海龟名称。

2.3.1 服务调用方式控制多个小海龟绘制彩色奥运五环

在vs code编译器中,在src/turtle_exercise/turtle_exercise/路径下,创建一个服务端文件spawn.py(~/ros2_ws1/src/turtle_exercise/turtle_exercise/spawn.py )打开该文件,输入下面代码:

python 复制代码
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from turtlesim.srv import Spawn, SetPen, Kill
from turtlesim.msg import Pose
import math
import time

class OlympicRingsDrawer(Node):
    def __init__(self):
        super().__init__('olympic_rings_drawer')
        
        # 1. 定义奥运五环的配置 (名称, 颜色RGB, 中心X, 中心Y)
        # 注意:turtlesim 窗口大小为 11x11,这里调整了坐标使其居中
        self.rings_config = [
            ("blue_ring",   (0, 120, 255), 3.0, 6.5),
            ("black_ring",  (0, 0, 0),     5.5, 6.5),
            ("red_ring",    (255, 0, 0),   8.0, 6.5),
            ("yellow_ring", (255, 255, 0), 4.25, 5.0),
            ("green_ring",  (0, 255, 0),   6.75, 5.0)
        ]
        
        self.radius = 1.2  # 环的半径
        self.linear_vel = 2.0  # 线速度
        self.angular_vel = self.linear_vel / self.radius  # 角速度 = v / r
        
        # 2. 创建服务客户端
        self.spawn_client = self.create_client(Spawn, '/spawn')
        self.kill_client = self.create_client(Kill, '/kill')
        
        # 3. 创建速度发布者字典和画笔设置客户端字典
        self.vel_pubs = {}
        self.pen_clients = {}
        
        # 4. 启动初始化流程
        self.init_sequence()

    def init_sequence(self):
        """按顺序执行:清背景 -> 杀默认海龟 -> 生成新海龟 -> 设颜色 -> 开始画"""
        self.set_background_white()
        self.kill_default_turtle()
        
        # 延迟生成海龟,确保背景设置完成
        self.create_timer(0.5, self.spawn_all_turtles)

    def set_background_white(self):
        """将背景设置为白色"""
        # turtlesim 没有直接的背景服务,但可以通过参数设置
        # 或者在 launch 文件中设置。这里我们使用 ros2 param 命令行替代
        # 实际上更好的做法是在 launch 里设置,但为了单文件逻辑,这里做个提示
        self.get_logger().info("请将仿真背景设置为白色(可通过 launch 参数或按键 'W')")

    def kill_default_turtle(self):
        """杀死默认的 turtle1"""
        while not self.kill_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('等待 /kill 服务...')
        req = Kill.Request()
        req.name = 'turtle1'
        self.kill_client.call_async(req)

    def spawn_all_turtles(self):
        """批量生成小海龟并设置画笔"""
        for name, color, x, y in self.rings_config:
            self.spawn_turtle(name, x + self.radius, y, 1.5708) # 从圆的右侧(3点钟方向)开始,朝上
            time.sleep(0.2) # 稍微等待,避免服务冲突
            self.set_pen_color(name, color)
            
        # 所有海龟生成完毕后,开始画圆
        self.create_timer(0.1, self.draw_rings)

    def spawn_turtle(self, name, x, y, theta):
        """生成一只小海龟"""
        while not self.spawn_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info(f'等待 /spawn 服务以生成 {name}...')
        req = Spawn.Request()
        req.name = name
        req.x = float(x)
        req.y = float(y)
        req.theta = float(theta)
        self.spawn_client.call_async(req)
        
        # 为这只海龟创建速度发布者
        self.vel_pubs[name] = self.create_publisher(Twist, f'/{name}/cmd_vel', 10)
        
    def set_pen_color(self, name, color):
        """设置小海龟的画笔颜色"""
        # 注意:turtlesim 原生不支持通过服务动态改颜色,
        # 但在某些修改版或通过参数可以。这里我们假设环境支持 /<name>/set_pen
        # 如果原生不支持,颜色可能不生效,但不影响画圆。
        # 标准做法是通过 launch 参数设置,这里用通用服务名代替
        try:
            pen_client = self.create_client(SetPen, f'/{name}/set_pen')
            if pen_client.wait_for_service(timeout_sec=1.0):
                req = SetPen.Request()
                req.r = color[0]
                req.g = color[1]
                req.b = color[2]
                req.width = 6
                pen_client.call_async(req)
        except Exception as e:
            self.get_logger().warn(f"设置 {name} 颜色失败,可能不支持该服务: {e}")

    def draw_rings(self):
        """控制所有海龟做圆周运动"""
        twist = Twist()
        twist.linear.x = self.linear_vel
        twist.angular.z = self.angular_vel
        
        for name, pub in self.vel_pubs.items():
            pub.publish(twist)

def main(args=None):
    rclpy.init(args=args)
    node = OlympicRingsDrawer()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

修改launch启动文件:spawn_launch.py

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription( # 1. 启动 turtlesim 仿真器,设置背景为白色 Node( package='turtlesim', executable='turtlesim_node', name='sim', parameters=\[ {'background_r': 255}, {'background_g': 255}, {'background_b': 255} ), # 2. 启动奥运五环绘制节点 Node( package='turtle_exercise', executable='spawn', name='olympic_rings_drawer', output='screen' ) ]) |

修改 setup.py文件

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| entry_points={ 'console_scripts': # 格式为:'可执行文件名 = 功能包名.文件名:主函数名' 'draw_spiral = turtle_exercise.draw_spiral:main', 'spawn = turtle_exercise.spawn:main', # 【确保有这一行】 , }, |

编译、加载、运行

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| # 1. 进入工作空间并编译 cd ~/ros2_ws1 colcon build --packages-select turtle_exercise # 2. 刷新环境变量 source install/setup.bash # 3. 启动奥运五环 ros2 launch turtle_exercise spawn_launch.py |
| root@LHAYR:~/ros2_ws1# colcon build Starting >>> turtle_exercise Finished <<< turtle_exercise 0.75s Summary: 1 package finished 0.91s root@LHAYR:~/ros2_ws1# source install/setup.bash root@LHAYR:~/ros2_ws1# ros2 launch turtle_exercise spawn_launch.py INFO launch: All log files can be found below /root/.ros/log/2026-07-10-20-02-36-318110-LHAYR-4818 INFO launch: Default logging verbosity is set to INFO INFO turtlesim_node-1: process started with pid 4821 INFO spawn-2: process started with pid 4822 turtlesim_node-1 INFO 1783684956.584694516 sim: Starting turtlesim with node name /sim turtlesim_node-1 INFO 1783684956.595242317 sim: Spawning turtle turtle1 at x=5.544445, y=5.544445, theta=0.000000 spawn-2 INFO 1783684956.654626313 olympic_rings_drawer: 请将仿真背景设置为白色(可通过 launch 参数或按键 'W') turtlesim_node-1 INFO 1783684957.429776807 sim: Spawning turtle blue_ring at x=4.200000, y=6.500000, theta=1.570800 |

2.4 仿真功能的示例-动作反馈

动作(Action)由两个核心角色组成,就像服务(Service)由服务端和客户端组成一样。但动作的角色分工更复杂,因为它是"异步长任务":

2.4.1 创建动作接口功能包-小海龟通过指令话多边形

在工作空间 `ros2_ws1/src` 下创建 `interfaces_exercise` 功能包(接口包必须使用ament_cmake:

复制代码
root@LHAYR:~/ros2_ws1# cd ~/ros2_ws1/src

root@LHAYR:~/ros2_ws1/src# ros2 pkg create --build-type ament\_cmake --license MIT interfaces\_exercise

going to create a new package

package name: interfaces_exercise

。。。。。。

creating folder ./interfaces_exercise/src

creating folder ./interfaces_exercise/include/interfaces_exercise

creating ./interfaces_exercise/CMakeLists.txt

root@LHAYR:~/ros2_ws1/src#

创建 action 文件夹与 Move.action

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| root@LHAYR:~/ros2_ws1/src# cd interfaces_exercise root@LHAYR:~/ros2_ws1/src/interfaces_exercise# mkdir action root@LHAYR:~/ros2_ws1/src/interfaces_exercise# touch action/Move.action root@LHAYR:~/ros2_ws1/src/interfaces_exercise# |

修改Move.action 动作接口:分为三部分:Goal(目标)、Result(结果)、Feedback(实时反馈),用vs code打开目录,。

|-------------------------------------------------------------------------------------------------------------------------------------|
| # Goal:需要绘制的多边形边数 int32 order --- # Result:最终所有顶点坐标序列 float32\[\] sequence --- # Feedback:当前已绘制的顶点坐标序列 float32\[\] partial_sequence |

修改 CMakeLists.txt,添加下面标红的语句。

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| cmake_minimum_required(VERSION 3.8) project(interfaces_exercise) 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(rosidl_default_generators REQUIRED) # uncomment the following section in order to fill in # further dependencies manually. # find_package(<dependency> REQUIRED) # 生成接口文件 rosidl_generate_interfaces(${PROJECT_NAME} "action/Move.action" ) if(BUILD_TESTING) |

修改 package.xml,添加下列4条标红的语句

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <?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>interfaces_exercise</name> <version>0.0.0</version> <description>TODO: Package description</description> <maintainer email="root@todo.todo">root</maintainer> <license>MIT</license> <buildtool_depend>ament_cmake</buildtool_depend> <buildtool_depend>rosidl_default_generators</buildtool_depend> <depend>action_msgs</depend> <exec_depend>rosidl_default_runtime</exec_depend> <test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_common</test_depend> <member_of_group>rosidl_interface_packages</member_of_group> <export> <build_type>ament_cmake</build_type> </export> </package> |

创建~/ros2_ws1/src/turtle_exercise/turtle_exercise/action_move.py 在action_move.py文件内输入下面代码

python 复制代码
import rclpy
import time
import math
from rclpy.node import Node
from rclpy.action import ActionServer
from geometry_msgs.msg import Twist
from interfaces_exercise.action import Move

class TurtleMoveActionServer(Node):
    def __init__(self):
        super().__init__('action_move_server')
        self.pub = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
        self._action_server = ActionServer(
            self,
            Move,
            '/turtle_move',
            self.execute_callback
        )
        self.get_logger().info("✅ 动作服务端已启动,等待指令:/turtle_move")

    def execute_callback(self, goal_handle):
        order = goal_handle.request.order
        self.get_logger().info(f'开始绘制 {order} 边形')

        feedback = Move.Feedback()
        result = Move.Result()
        sequence = []

        angle = 2 * math.pi / order

        for i in range(order):
            # 前进
            twist = Twist()
            twist.linear.x = 1.2
            self.pub.publish(twist)
            time.sleep(1.0)

            # 记录顶点并反馈
            sequence.append(float(i + 1))
            feedback.partial_sequence = sequence
            goal_handle.publish_feedback(feedback)
            self.get_logger().info(f'反馈:已到达第 {i+1} 个顶点')

            # 转弯
            twist.linear.x = 0.0
            twist.angular.z = angle
            self.pub.publish(twist)
            time.sleep(1.0)

        # 停止
        self.pub.publish(Twist())
        goal_handle.succeed()
        result.sequence = sequence
        self.get_logger().info("✅ 多边形绘制完成!")
        return result

def main(args=None):
    rclpy.init(args=args)
    server = TurtleMoveActionServer()
    rclpy.spin(server)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

创建launch文件,路径: ~/ros2_ws1/src/turtle_exercise/launch/action_move_launch.py

在action_move_launch.py文件内输入如下代码

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from launch import LaunchDescription from launch_ros.actions import Node from launch.actions import DeclareLaunchArgument, ExecuteProcess from launch.substitutions import LaunchConfiguration def generate_launch_description(): return LaunchDescription( DeclareLaunchArgument('order', default_value='6'), Node( package='turtlesim', executable='turtlesim_node', name='sim' ), Node( package='turtle_exercise', executable='action_move', name='action_move_node' ), ExecuteProcess( cmd=\[ 'ros2 action send_goal --feedback /turtle_move', 'interfaces_exercise/action/Move', '\\'{"order":', LaunchConfiguration('order'), '}\\'' , shell=True ) ]) |

setup.py 最终配置:添加'action_move = turtle_exercise.action_move:main',

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| entry_points={ 'console_scripts': # 格式为:'可执行文件名 = 功能包名.文件名:主函数名' 'draw_spiral = turtle_exercise.draw_spiral:main', 'spawn = turtle_exercise.spawn:main', 'action_move = turtle_exercise.action_move:main', , }, |

编译,加载,运行

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| root@LHAYR:~/ros2_ws1# cd ~/ros2_ws1 root@LHAYR:~/ros2_ws1# colcon build 0.216s WARNING:colcon.colcon_ros.prefix_path.ament:The path '/root/ros2_ws1/install/turtle_exercise' in the environment variable AMENT_PREFIX_PATH doesn't exist Starting >>> interfaces_exercise Starting >>> turtle_exercise Finished <<< interfaces_exercise 0.33s Finished <<< turtle_exercise 0.79s Summary: 2 packages finished 0.94s root@LHAYR:~/ros2_ws1# source install/setup.bash root@LHAYR:~/ros2_ws1# ros2 launch turtle\_exercise action\_move\_launch.py order:=6 INFO launch: All log files can be found below /root/.ros/log/2026-07-10-21-52-27-126167-LHAYR-6574 INFO launch: Default logging verbosity is set to INFO INFO turtlesim_node-1: process started with pid 6577 INFO action_move-2: process started with pid 6578 INFO turtle_move-3: process started with pid 6579 turtlesim_node-1 INFO 1783691547.401203064 sim: Starting turtlesim with node name /sim turtlesim_node-1 INFO |

第三章 实际开发中的选型指南

通过"画五角星"和"画奥运五环"这两个具体的案例,对比话题(Topic)和服务(Service),说明你已经真正跨入了 ROS 2 开发的大门。在 ROS 2 中,话题和服务就像是机器人的"神经系统"和"肌肉系统",它们各有分工。下面我为你详细梳理它们的优缺点,以及在实际开发中的选型指南。

3.1、 话题(Topic)与服务(Service)的优缺点对比

表格

对比维度 话题(Topic)通信 服务(Service)通信
通信模型 发布/订阅模型(Publish/Subscribe) 请求/响应模型(Request/Response)
数据流向 单向(一对多、多对一、多对多) 双向(一对一)
是否阻塞 非阻塞:发布者发完就忘,不管有没有人听 阻塞/异步等待:客户端发出请求后,必须等服务端处理完并返回结果
数据特性 连续、高频、流式数据 离散、低频、状态/指令数据
核心优点 解耦极强,实时性高,适合持续的数据流传输 逻辑清晰,有明确的结果反馈,适合任务确认
核心缺点 无法确认对方是否收到,没有返回值 如果服务端卡死,客户端会一直等待(阻塞)

3.2、 结合你的案例来分析

3.2.1. 为什么"画五角星"用话题(Topic)?

  • 场景特征:控制小海龟移动需要高频、连续地发送速度指令(例如每 0.1 秒发一次)。
  • 选择理由:话题是"单向非阻塞"的。你的控制节点只管疯狂往 /cmd_vel 话题里发速度指令,不需要等 turtlesim_node 回复"我收到了"。这保证了机器人控制的实时性。如果用服务来发速度指令,每次发完都要等回复,系统会因为频繁的网络握手而严重卡顿,五角星就画歪了。

3.2.2. 为什么"画奥运五环"用服务(Service)?

  • 场景特征:生成海龟(/spawn)、设置画笔颜色(/set_pen)、清除背景(/clear)都是一次性、低频的操作。
  • 选择理由:服务是"请求-响应"的。当你调用 /spawn 时,你需要明确知道这只海龟有没有成功生成,如果名字重复了,服务端还会返回错误提示。这种需要"确认结果"的任务,用服务最合适。

3.3、 实战指南:开发时如何选择?

在实际的机器人功能开发中,你可以用以下**"三个灵魂拷问"**来决定使用哪种通信机制:

💡 拷问 1:这个数据是"连续流"还是"一次性指令"?

  • 选话题(Topic):如果数据像水流一样源源不断。
    • 例子:激光雷达数据、摄像头图像、底盘实时速度、电机编码器反馈。
  • 选服务(Service):如果数据像按门铃,按一下响一次。
    • 例子:拍照保存、切换导航地图、开关机械爪、查询电池电量。

💡 拷问 2:发送方需不需要"确认回执"?

  • 选话题(Topic):发出去就不管了,追求极致的速度。
    • 例子:避障时的紧急刹车指令(发出去最重要,等回执就来不及撞墙了)。
  • 选服务(Service):必须等对方说"OK"才能进行下一步。
    • 例子:调用机械臂抓取杯子。你必须等服务端返回 success=True,才能执行下一个"放下杯子"的动作。

💡 拷问 3:通信的节点数量是多少?

  • 选话题(Topic):一对多广播。
    • 例子:一个里程计节点发布位置,同时给"导航模块"、"UI显示模块"、"日志记录模块"用。
  • 选服务(Service):严格的一对一请求。
    • 例子:主控板专门向"传感器驱动板"请求一次标定数据。

🌟 总结你的"奥运五环"项目

其实你刚才画的奥运五环,是一个**"话题 + 服务"完美结合**的经典案例:

  1. 服务(Service)负责"搭台":调用 /spawn 和 /set_pen 把 5 只海龟按位置放好,换好颜色。
  2. 话题(Topic)负责"唱戏":在 5 只海龟就位后,通过 /cmd_vel 话题持续发布速度,让它们同时画圆。

掌握了这个选型逻辑,你以后在做复杂的机器人项目(比如让小车一边建图、一边导航、一边识别物体)时,就能游刃有余地设计系统架构了!

3.4目录结构总览

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ROS2_WS1/ ├── build/ # 编译产物目录(自动生成) │ └── 存放各功能包的中间编译文件、构建配置 ├── install/ # 安装目录(自动生成) │ └── 存放编译后可执行文件、库、启动文件等运行时资源 ├── log/ # 日志目录(自动生成) │ └── 存放 ROS2 运行时日志、调试信息 └── src/ # 源代码根目录(手动开发) ├── interfaces_exercise/ # 自定义接口功能包(ament_cmake 类型) │ ├── action/ │ │ └── Move.action # 自定义动作接口:定义多边形绘制的 Goal/Result/Feedback │ ├── include/ # 自动生成的 C++ 接口头文件 │ ├── src/ # 自动生成的 C++ 接口实现文件 │ ├── CMakeLists.txt # CMake 构建配置:编译动作/消息/服务接口 │ ├── LICENSE # 许可证文件 │ └── package.xml # 包元数据:声明依赖、导出接口 └── turtle_exercise/ # 业务功能包(ament_python 类型) ├── launch/ # 启动文件目录 │ ├── spawn/ # 子目录:存放 spawn 相关启动文件 │ ├── action_move_launch.py # 动作案例启动文件:启动 turtlesim + 动作服务端 + 发送目标 │ ├── spawn_launch.py # 多海龟生成启动文件:启动 turtlesim + 生成奥运五环海龟 │ └── spiral_launch.py # 螺旋/图形绘制启动文件:启动 turtlesim + 绘制节点 ├── resource/ # 资源目录 │ └── turtle_exercise # 包标识文件(ament_python 必需) ├── test/ # 自动化测试目录 │ ├── test_copyright.py # 版权检查测试 │ ├── test_flake8.py # 代码风格检查(flake8) │ └── test_pep257.py # 文档风格检查(PEP257) ├── turtle_exercise/ # Python 模块根目录(存放所有节点代码) │ ├── init.py # Python 包初始化文件 │ ├── action_move.py # 动作服务端节点:接收动作目标,控制海龟绘制多边形 │ ├── draw_spiral.py # 绘制节点:控制海龟绘制螺旋/正方形/五角星等图形 │ ├── draw_spiral01.py # 绘制节点变体:备用/改进版图形绘制 │ └── spawn.py # 服务调用节点:调用 /spawn 服务生成多只海龟 ├── LICENSE # 许可证文件 ├── package.xml # 包元数据:声明依赖、导出 Python 节点 ├── setup.cfg # Python 包配置(测试、代码风格) └── setup.py # Python 包构建配置:注册节点、安装启动文件等 |

相关推荐
Excel效率人3 小时前
飞书自动回复机器人开发:@机器人自助查询物流信息
机器人·node.js·飞书
茯苓gao21 小时前
嵌入式开发笔记:工业机器人通信协议深度解析——从现场总线到工业以太网
笔记·嵌入式硬件·学习·机器人·信息与通信
鲁邦通物联网1 天前
基于GPIO与高频滤波的状态机:非侵入式机器人梯控精准平层判定实现
机器人·巡检机器人·机器人梯控·agv梯控·非侵入式采集·机器人乘梯·机器人自主乘梯
牛哥带你学代码1 天前
VLA 与机器人基础模型:具身智能的下一代操作系统
人工智能·机器人
code_pgf1 天前
大模型赋能家庭服务机器人的场景适应性分析
大数据·人工智能·机器人
深度森林2 天前
机器人领域“工业机械臂”高价值专利案例:一种工业机器人智能分拣方法
机器人·工业机械臂
XMan_Liu2 天前
AI聊天机器人架构学习
人工智能·架构·机器人
田里的水稻2 天前
EP_XML\JSON配置文件和YAML
xml·运维·人工智能·机器人·自动驾驶·json